From f7813ffd6cb8493a532abbca361bb2af39c4b65d Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 13 Feb 2024 13:07:11 -0500 Subject: [PATCH 01/48] track vyper master branch --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 178d4059..dd46b1ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = ["Topic :: Software Development"] # Requirements dependencies = [ - "vyper >= 0.3.10", + "vyper @ git+https://github.com/vyperlang/vyper.git@master", "eth-stdlib>=0.2.7,<0.3.0", "eth-abi", "py-evm>=0.7.0a4", From 672b16814875224d4e76a19cfe2feb86c7ec6be8 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 13 Feb 2024 14:17:21 -0500 Subject: [PATCH 02/48] update boa to use new APIs --- boa/contracts/vyper/compiler_utils.py | 10 ++-- boa/contracts/vyper/ir_executor.py | 14 +++-- boa/contracts/vyper/vyper_contract.py | 79 ++++++++++----------------- boa/interpret.py | 31 ++++++----- boa/precompile.py | 6 +- boa/profiling.py | 22 ++++---- 6 files changed, 75 insertions(+), 87 deletions(-) diff --git a/boa/contracts/vyper/compiler_utils.py b/boa/contracts/vyper/compiler_utils.py index 12ff86f1..aa2cd0d6 100644 --- a/boa/contracts/vyper/compiler_utils.py +++ b/boa/contracts/vyper/compiler_utils.py @@ -3,9 +3,9 @@ import vyper.ast as vy_ast import vyper.semantics.analysis as analysis -from vyper.ast.utils import parse_to_ast +from vyper.ast.parse import parse_to_ast from vyper.codegen.core import anchor_opt_level -from vyper.codegen.function_definitions import generate_ir_for_function +from vyper.codegen.function_definitions import generate_ir_for_external_function from vyper.codegen.ir_node import IRnode from vyper.evm.opcodes import anchor_evm_version from vyper.exceptions import InvalidType @@ -36,7 +36,7 @@ def compile_vyper_function(vyper_function, contract): compiler_data = contract.compiler_data with anchor_compiler_settings(compiler_data): - global_ctx = contract.global_ctx + module_t = contract.global_ctx ifaces = compiler_data.interface_codes ast = parse_to_ast(vyper_function, ifaces) vy_ast.folding.fold(ast) @@ -49,8 +49,8 @@ def compile_vyper_function(vyper_function, contract): ast = ast.body[0] func_t = ast._metadata["type"] - external_func_info = generate_ir_for_function(ast, global_ctx, False) - ir = external_func_info.common_ir + funcinfo = generate_ir_for_external_function(ast, module_t) + ir = funcinfo.common_ir entry_label = func_t._ir_info.external_function_base_entry_label diff --git a/boa/contracts/vyper/ir_executor.py b/boa/contracts/vyper/ir_executor.py index 30f1657d..23795636 100644 --- a/boa/contracts/vyper/ir_executor.py +++ b/boa/contracts/vyper/ir_executor.py @@ -13,7 +13,7 @@ from eth_hash.auto import keccak from vyper.compiler.phases import CompilerData from vyper.evm.opcodes import OPCODES -from vyper.utils import mkalphanum, unsigned_to_signed +from vyper.utils import unsigned_to_signed from boa.util.lrudict import lrudict from boa.vm.fast_mem import FastMem @@ -25,6 +25,9 @@ def keccak256(x): return _keccak_cache.setdefault_lambda(x, keccak) +def _mkalphanum(string): + # map a string to only-alphanumeric chars + return "".join([c if c.isalnum() else "_" for c in s]) @dataclass class _Line: @@ -95,11 +98,12 @@ def freshvar(self, name=""): return f"var_{name}_{self.var_id}" @cached_property - def contract_name(self): - return mkalphanum(PurePath(self.vyper_compiler_data.contract_name).name) + def contract_path(self): + return PurePath(self.vyper_compiler_data.contract_path).name def translate_label(self, label): - return f"{self.contract_name}_{self.uuid}_{label}" + name = _mkalphanum(self.contract_path) + return f"{self.contract_path}_{self.uuid}_{label}" def add_unique_symbol(self, symbol): if symbol in self.unique_symbols: @@ -1130,7 +1134,7 @@ def executor_from_ir(ir_node, vyper_compiler_data) -> Any: # TODO: rename this, this is "something.vy", but we maybe want # "something.py " - ret.compile_main(vyper_compiler_data.contract_name) + ret.compile_main(vyper_compiler_data.contract_path) return ret diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 15c69155..1755223f 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -15,11 +15,12 @@ import vyper.semantics.analysis as analysis import vyper.semantics.namespace as vy_ns from eth.exceptions import VMError -from vyper.ast.utils import parse_to_ast +from vyper.ast.parse import parse_to_ast from vyper.codegen.core import anchor_opt_level, calculate_type_for_external_return -from vyper.codegen.function_definitions import generate_ir_for_function -from vyper.codegen.function_definitions.common import ExternalFuncIR, InternalFuncIR -from vyper.codegen.global_context import GlobalContext +from vyper.codegen.function_definitions import ( + generate_ir_for_external_function, + generate_ir_for_internal_function, +) from vyper.codegen.ir_node import IRnode from vyper.codegen.module import generate_ir_for_module from vyper.compiler import CompilerData @@ -406,7 +407,8 @@ def get(self, truncate_limit=None): class StorageModel: def __init__(self, contract): compiler_data = contract.compiler_data - for k, v in compiler_data.global_ctx.variables.items(): + # TODO: recurse into imported modules + for k, v in contract.module_t.variables.items(): is_storage = not v.is_immutable and not v.is_constant if is_storage: slot = compiler_data.storage_layout["storage_layout"][k]["slot"] @@ -429,7 +431,8 @@ class ImmutablesModel: def __init__(self, contract): compiler_data = contract.compiler_data data_section = memoryview(contract.data_section) - for k, v in compiler_data.global_ctx.variables.items(): + # TODO: recurse into imported modules + for k, v in contract.module_t.variables.items(): if v.is_immutable: # check that v ofst = compiler_data.storage_layout["code_layout"][k]["offset"] immutable_raw_bytes = data_section[ofst:] @@ -460,29 +463,29 @@ def __init__( self.created_from = created_from # add all exposed functions from the interface to the contract - external_fns = { + exposed_fns = { fn.name: fn - for fn in self.global_ctx.functions - if fn._metadata["type"].is_external + for fn in self.module_t.function_defs + if not fn._metadata["func_type"].is_internal } # set external methods as class attributes: self._ctor = None - if "__init__" in external_fns: - self._ctor = VyperFunction(external_fns.pop("__init__"), self) + if "__init__" in exposed_fns: + self._ctor = VyperFunction(exposed_fns.pop("__init__"), self) if skip_initcode: self._address = Address(override_address) else: self._address = self._run_init(*args, override_address=override_address) - for fn_name, fn in external_fns.items(): + for fn_name, fn in exposed_fns.items(): setattr(self, fn_name, VyperFunction(fn, self)) # set internal methods as class.internal attributes: self.internal = lambda: None - for fn in self.global_ctx.functions: - if not fn._metadata["type"].is_internal: + for fn in self.module_t.function_defs: + if not fn._metadata["func_type"].is_internal: continue setattr(self.internal, fn.name, VyperInternalFunction(fn, self)) @@ -519,7 +522,7 @@ def _set_bytecode(self, bytecode: bytes) -> None: def __repr__(self): ret = ( - f"<{self.compiler_data.contract_name} at {self.address}, " + f"<{self.compiler_data.contract_path} at {self.address}, " f"compiled with vyper-{vyper.__version__}+{vyper.__commit__}>" ) @@ -580,7 +583,7 @@ def debug_frame(self, computation=None): return frame_detail @property - def global_ctx(self): + def module_t(self): return self.compiler_data.global_ctx @property @@ -723,30 +726,7 @@ def line_profile(self, computation=None): @cached_property def _ast_module(self): - module = copy.deepcopy(self.compiler_data.vyper_module) - - # do the same thing as vyper_module_folded but skip getter expansion - with anchor_compiler_settings(self.compiler_data): - vy_ast.folding.fold(module) - with vy_ns.get_namespace().enter_scope(): - analysis.add_module_namespace( - module, self.compiler_data.interface_codes - ) - analysis.validate_functions(module) - # we need to cache the namespace right here(!). - # set_data_positions will modify the type definitions in place. - self._cache_namespace(vy_ns.get_namespace()) - - vy_ast.expansion.remove_unused_statements(module) - # calculate slots for all storage variables, tagging - # the types in the namespace. - set_data_positions(module, storage_layout_overrides=None) - - # ensure _ir_info is generated for all functions in this copied/shadow - # namespace - _ = generate_ir_for_module(GlobalContext(module)) - - return module + module = copy.deepcopy(self.compiler_data.annotated_vyper_module) # the global namespace is expensive to compute, so cache it def _cache_namespace(self, namespace): @@ -786,7 +766,7 @@ def unoptimized_assembly(self): @cached_property def data_section_size(self): - return self.global_ctx.immutable_section_bytes + return self.module_t.immutable_section_bytes @cached_property def data_section(self): @@ -809,7 +789,7 @@ def unoptimized_ir(self): with anchor_opt_level(OptimizationLevel.NONE), anchor_evm_version( self.compiler_data.settings.evm_version ): - return generate_ir_for_module(self.compiler_data.global_ctx) + return generate_ir_for_module(self.compiler_data.module_t) @cached_property def ir_executor(self): @@ -884,10 +864,10 @@ def __init__(self, fn_ast, contract): self.__doc__ = ( fn_ast.doc_string.value if hasattr(fn_ast, "doc_string") else None ) - self.__module__ = self.contract.compiler_data.contract_name + self.__module__ = self.contract.compiler_data.contract_path def __repr__(self): - return f"{self.contract.compiler_data.contract_name}.{self.fn_ast.name}" + return f"{self.contract.compiler_data.contract_path}.{self.fn_ast.name}" def __str__(self): return repr(self.func_t) @@ -898,16 +878,17 @@ def _source_map(self): @property def func_t(self): - return self.fn_ast._metadata["type"] + return self.fn_ast._metadata["func_type"] @cached_property def ir(self): - global_ctx = self.contract.global_ctx + module_t = self.contract.module_t - res = generate_ir_for_function(self.fn_ast, global_ctx, False) - if isinstance(res, InternalFuncIR): + if self.func_t.is_internal: + res = generate_ir_for_internal_function(self.fn_ast, module_t, False) ir = res.func_ir - elif isinstance(res, ExternalFuncIR): + else: + res = generate_ir_for_external_function(self.fn_ast, module_t) ir = res.common_ir return optimize(ir) diff --git a/boa/interpret.py b/boa/interpret.py index 61d978ce..d4ea8229 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -4,8 +4,9 @@ from typing import Any, Union import vyper -from vyper.cli.vyper_compile import get_interface_codes +from vyper.cli.vyper_compile import get_search_paths from vyper.compiler.phases import CompilerData +from vyper.compiler.input_bundle import FileInput, FilesystemInputBundle from boa.contracts.abi.abi_contract import ABIContractFactory from boa.contracts.vyper.compiler_utils import anchor_compiler_settings @@ -22,6 +23,7 @@ _disk_cache = None +_search_path = None def set_cache_dir(cache_dir="~/.cache/titanoboa"): @@ -33,22 +35,23 @@ def set_cache_dir(cache_dir="~/.cache/titanoboa"): _disk_cache = DiskCache(cache_dir, compiler_version) -def compiler_data(source_code: str, contract_name: str, **kwargs) -> CompilerData: - global _disk_cache +def set_search_path(path: list[str]): + global _search_path + _search_path = path - def _ifaces(): - # use get_interface_codes to get the interface source dict - # TODO revisit this once imports are cleaned up vyper-side - ret = get_interface_codes(Path("."), {contract_name: source_code}) - return ret[contract_name] - if _disk_cache is None: - ifaces = _ifaces() - ret = CompilerData(source_code, contract_name, interface_codes=ifaces, **kwargs) - return ret +def compiler_data(source_code: str, contract_name: str, filename: str, **kwargs) -> CompilerData: + global _disk_cache, _search_path + + # TODO: figure out how caching works with modules. + if True: + file_input = FileInput(source_code=source_code, source_id=-1, path=Path(contract_name), resolved_path=Path(contract_name)) + search_paths = get_search_paths(_search_path) + input_bundle = FilesystemInputBundle(search_paths) + return CompilerData(file_input, input_bundle, **kwargs) def func(): - ifaces = _ifaces() + raise Exception("unreachable") ret = CompilerData(source_code, contract_name, interface_codes=ifaces, **kwargs) with anchor_compiler_settings(ret): _ = ret.bytecode, ret.bytecode_runtime # force compilation to happen @@ -106,7 +109,7 @@ def loads_partial( compiler_args = compiler_args or {} - data = compiler_data(source_code, name, **compiler_args) + data = compiler_data(source_code, name, filename, **compiler_args) return VyperDeployer(data, filename=filename) diff --git a/boa/precompile.py b/boa/precompile.py index 2e1ace94..5b83517d 100644 --- a/boa/precompile.py +++ b/boa/precompile.py @@ -1,7 +1,7 @@ from typing import Any from vyper.ast import parse_to_ast -from vyper.builtins._signatures import BuiltinFunction +from vyper.builtins._signatures import BuiltinFunctionT from vyper.builtins.functions import DISPATCH_TABLE, STMT_DISPATCH_TABLE from vyper.builtins.functions import abi_encode as abi_encode_ir from vyper.builtins.functions import ir_tuple_from_args, process_inputs @@ -17,9 +17,9 @@ from boa.util.abi import abi_decode, abi_encode -class PrecompileBuiltin(BuiltinFunction): +class PrecompileBuiltin(BuiltinFunctionT): def __init__(self, name, args, return_type, address): - # override BuiltinFunction attributes + # override BuiltinFunctionT attributes self._id = name self._inputs = args # list[tuple[str, VyperType]] self._return_type = return_type diff --git a/boa/profiling.py b/boa/profiling.py index c50b9f5b..4afea4fa 100644 --- a/boa/profiling.py +++ b/boa/profiling.py @@ -14,7 +14,7 @@ @dataclass(unsafe_hash=True) class LineInfo: address: str - contract_name: str + contract_path: str lineno: int line_src: str fn_name: str @@ -23,7 +23,7 @@ class LineInfo: @dataclass(unsafe_hash=True) class ContractMethodInfo: address: str - contract_name: str + contract_path: str fn_name: str @@ -186,7 +186,7 @@ def summary( for (contract, line), datum in raw_summary: data = ", ".join(f"{c}: {getattr(datum, c)}" for c in display_columns) line_src = get_line(contract.compiler_data.source_code, line) - x = f"{contract.address}:{contract.compiler_data.contract_name}:{line} {data}" + x = f"{contract.address}:{contract.compiler_data.contract_path}:{line} {data}" tmp.append((x, line_src)) just = max(len(t[0]) for t in tmp) @@ -204,7 +204,7 @@ def get_line_data(self): # here we use net_gas to include child computation costs: line_info = LineInfo( address=contract.address, - contract_name=contract.compiler_data.contract_name, + contract_path=contract.compiler_data.contract_path, lineno=line, line_src=get_line(contract.compiler_data.source_code, line), fn_name=fn_name, @@ -225,7 +225,7 @@ def __repr__(self): def cache_gas_used_for_computation(contract, computation): profile = contract.line_profile(computation) env = contract.env - contract_name = contract.compiler_data.contract_name + contract_path = contract.compiler_data.contract_path # -------------------- CACHE CALL PROFILE -------------------- # get gas used. We use Datum().net_gas here instead of Datum().net_tot_gas @@ -242,7 +242,7 @@ def cache_gas_used_for_computation(contract, computation): fn_name = fn.name fn = ContractMethodInfo( - contract_name=contract_name, + contract_path=contract_path, address=to_checksum_address(contract.address), fn_name=fn_name, ) @@ -334,7 +334,7 @@ def get_call_profile_table(env: Env) -> Table: cname = "" caddr = "" if c == 0: - cname = profile.contract_name + cname = profile.contract_path caddr = address fn_name = profile.fn_name table.add_row(cname, caddr, fn_name, *stats.net_gas_stats.get_str_repr()) @@ -347,7 +347,7 @@ def get_call_profile_table(env: Env) -> Table: def get_line_profile_table(env: Env) -> Table: contracts: dict = {} for lp, gas_data in env._cached_line_profiles.items(): - contract_uid = (lp.contract_name, lp.address) + contract_uid = (lp.contract_path, lp.address) # add spaces so numbers take up equal space lineno = str(lp.lineno).rjust(3) @@ -358,8 +358,8 @@ def get_line_profile_table(env: Env) -> Table: ) table = _create_table(for_line_profile=True) - for (contract_name, contract_address), fn_data in contracts.items(): - contract_file_path = os.path.split(contract_name) + for (contract_path, contract_address), fn_data in contracts.items(): + contract_file_path = os.path.split(contract_path) contract_data_str = ( f"Path: {contract_file_path[0]}\n" f"Name: {contract_file_path[1]}\n" @@ -385,7 +385,7 @@ def get_line_profile_table(env: Env) -> Table: if code.endswith("\n"): code = code[:-1] stats = Stats(gas_used) - data = (contract_name, fn_name, code, *stats.get_str_repr()) + data = (contract_path, fn_name, code, *stats.get_str_repr()) l_profile.append(data) # sorted by mean (x[4]): From 725d4bd8496dec4d036191fb1795a253386c9d1f Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 13 Feb 2024 15:07:14 -0500 Subject: [PATCH 03/48] update unit tests to use new @deploy modifier --- examples/ERC20.vy | 2 +- examples/deployer.vy | 2 +- tests/integration/RewardPool.vy | 2 +- tests/integration/network/anvil/test_network_env.py | 2 +- tests/integration/network/sepolia/test_sepolia_env.py | 2 +- tests/integration/veYFI.vy | 2 +- tests/unitary/test_gas_profiling.py | 2 +- tests/unitary/test_isolation.py | 2 +- 8 files changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/ERC20.vy b/examples/ERC20.vy index 7d379785..521406c3 100644 --- a/examples/ERC20.vy +++ b/examples/ERC20.vy @@ -34,7 +34,7 @@ totalSupply: public(uint256) minter: address -@external +@deploy def __init__(_name: String[32], _symbol: String[32], _decimals: uint8, supply: uint256): init_supply: uint256 = supply * 10 ** convert(decimals, uint256) name = _name diff --git a/examples/deployer.vy b/examples/deployer.vy index fef73f74..6f5ebaae 100644 --- a/examples/deployer.vy +++ b/examples/deployer.vy @@ -2,7 +2,7 @@ from vyper.interfaces import ERC20 BLUEPRINT: immutable(address) -@external +@deploy def __init__(blueprint_address: address): BLUEPRINT = blueprint_address diff --git a/tests/integration/RewardPool.vy b/tests/integration/RewardPool.vy index e9152aa3..e8f054dd 100644 --- a/tests/integration/RewardPool.vy +++ b/tests/integration/RewardPool.vy @@ -64,7 +64,7 @@ token_last_balance: public(uint256) ve_supply: public(HashMap[uint256, uint256]) -@external +@deploy def __init__(veyfi: VotingYFI, start_time: uint256): """ @notice Contract constructor diff --git a/tests/integration/network/anvil/test_network_env.py b/tests/integration/network/anvil/test_network_env.py index 13ad59d5..1f0acf10 100644 --- a/tests/integration/network/anvil/test_network_env.py +++ b/tests/integration/network/anvil/test_network_env.py @@ -9,7 +9,7 @@ totalSupply: public(uint256) balances: HashMap[address, uint256] -@external +@deploy def __init__(t: uint256): self.totalSupply = t self.balances[self] = t diff --git a/tests/integration/network/sepolia/test_sepolia_env.py b/tests/integration/network/sepolia/test_sepolia_env.py index d287601e..f87be433 100644 --- a/tests/integration/network/sepolia/test_sepolia_env.py +++ b/tests/integration/network/sepolia/test_sepolia_env.py @@ -10,7 +10,7 @@ totalSupply: public(uint256) balances: HashMap[address, uint256] -@external +@deploy def __init__(t: uint256): self.totalSupply = t self.balances[self] = t diff --git a/tests/integration/veYFI.vy b/tests/integration/veYFI.vy index 16d69498..b98b0a86 100644 --- a/tests/integration/veYFI.vy +++ b/tests/integration/veYFI.vy @@ -76,7 +76,7 @@ point_history: public(HashMap[address, HashMap[uint256, Point]]) # epoch -> uns slope_changes: public(HashMap[address, HashMap[uint256, int128]]) # time -> signed slope change -@external +@deploy def __init__(token: ERC20, reward_pool: RewardPool): """ @notice Contract constructor diff --git a/tests/unitary/test_gas_profiling.py b/tests/unitary/test_gas_profiling.py index f834175c..ed562606 100644 --- a/tests/unitary/test_gas_profiling.py +++ b/tests/unitary/test_gas_profiling.py @@ -30,7 +30,7 @@ def foo(a: uint256) -> uint256: view FOO: immutable(address) -@external +@deploy def __init__(_foo_address: address): FOO = _foo_address diff --git a/tests/unitary/test_isolation.py b/tests/unitary/test_isolation.py index 164acaa8..d846868b 100644 --- a/tests/unitary/test_isolation.py +++ b/tests/unitary/test_isolation.py @@ -15,7 +15,7 @@ def boa_contract(): a: public(uint256) b: public(address) -@external +@deploy def __init__(a_input: uint256, b_input: address): self.a = a_input From 592c6023057fc197fee744bfff765eb5c35aa525 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 13 Feb 2024 16:26:29 -0500 Subject: [PATCH 04/48] fix internal functions --- boa/contracts/vyper/compiler_utils.py | 34 ++++++++++++++++++++------- boa/contracts/vyper/ir_executor.py | 20 +++++++++------- boa/contracts/vyper/vyper_contract.py | 29 +++++++++++++++++++---- boa/interpret.py | 15 ++++++++---- 4 files changed, 71 insertions(+), 27 deletions(-) diff --git a/boa/contracts/vyper/compiler_utils.py b/boa/contracts/vyper/compiler_utils.py index aa2cd0d6..e37aec31 100644 --- a/boa/contracts/vyper/compiler_utils.py +++ b/boa/contracts/vyper/compiler_utils.py @@ -5,8 +5,12 @@ import vyper.semantics.analysis as analysis from vyper.ast.parse import parse_to_ast from vyper.codegen.core import anchor_opt_level -from vyper.codegen.function_definitions import generate_ir_for_external_function +from vyper.codegen.function_definitions import ( + generate_ir_for_external_function, + generate_ir_for_internal_function, +) from vyper.codegen.ir_node import IRnode +from vyper.codegen.module import _globally_reachable_functions from vyper.evm.opcodes import anchor_evm_version from vyper.exceptions import InvalidType from vyper.ir import compile_ir, optimizer @@ -36,19 +40,18 @@ def compile_vyper_function(vyper_function, contract): compiler_data = contract.compiler_data with anchor_compiler_settings(compiler_data): - module_t = contract.global_ctx - ifaces = compiler_data.interface_codes - ast = parse_to_ast(vyper_function, ifaces) - vy_ast.folding.fold(ast) + module_t = contract.module_t + ast = parse_to_ast(vyper_function) # override namespace and add wrapper code at the top with contract.override_vyper_namespace(): - analysis.add_module_namespace(ast, ifaces) - analysis.validate_functions(ast) + analysis.validate_semantics(ast, compiler_data.input_bundle) ast = ast.body[0] - func_t = ast._metadata["type"] + func_t = ast._metadata["func_type"] + if func_t._function_id is None: + func_t._function_id = contract._get_function_id() funcinfo = generate_ir_for_external_function(ast, module_t) ir = funcinfo.common_ir @@ -63,7 +66,20 @@ def compile_vyper_function(vyper_function, contract): # all labels are present, and then optimize all together # (use unoptimized IR, ir_executor can't handle optimized selector tables) _, contract_runtime = contract.unoptimized_ir - ir = IRnode.from_list(["seq", ir, contract_runtime]) + ir_list = ["seq", ir, contract_runtime] + reachable = func_t.reachable_internal_functions + already_compiled = _globally_reachable_functions(module_t.function_defs) + missing_functions = reachable.difference(already_compiled) + # TODO: cache function compilations or something + for f in missing_functions: + assert f.ast_def is not None + if f._function_id is None: + f._function_id = contract._get_function_id() + ir_list.append( + generate_ir_for_internal_function(f.ast_def, module_t, False).func_ir + ) + + ir = IRnode.from_list(ir_list) ir = optimizer.optimize(ir) assembly = compile_ir.compile_to_assembly(ir) diff --git a/boa/contracts/vyper/ir_executor.py b/boa/contracts/vyper/ir_executor.py index 23795636..20eed0f0 100644 --- a/boa/contracts/vyper/ir_executor.py +++ b/boa/contracts/vyper/ir_executor.py @@ -25,9 +25,11 @@ def keccak256(x): return _keccak_cache.setdefault_lambda(x, keccak) + def _mkalphanum(string): # map a string to only-alphanumeric chars - return "".join([c if c.isalnum() else "_" for c in s]) + return "".join([c if c.isalnum() else "_" for c in string]) + @dataclass class _Line: @@ -98,12 +100,11 @@ def freshvar(self, name=""): return f"var_{name}_{self.var_id}" @cached_property - def contract_path(self): - return PurePath(self.vyper_compiler_data.contract_path).name + def contract_name(self): + return _mkalphanum(PurePath(self.vyper_compiler_data.contract_path).name) def translate_label(self, label): - name = _mkalphanum(self.contract_path) - return f"{self.contract_path}_{self.uuid}_{label}" + return _mkalphanum(f"{self.contract_name}_{self.uuid}_{label}") def add_unique_symbol(self, symbol): if symbol in self.unique_symbols: @@ -239,7 +240,7 @@ def compile(self, out=None, out_typ=None): def _compile(self, context): raise RuntimeError("must be overridden in subclass!") - def compile_main(self, contract_path=""): + def compile_main(self, contract_name=""): self.builder.extend("import vyper.utils\nimport _operator") main_name = self.compile_ctx.translate_label("main") @@ -251,7 +252,7 @@ def compile_main(self, contract_path=""): self.builder.extend("\n\n") func.compile_func() - py_file = contract_path + str(self.compile_ctx.uuid) + ".py" + py_file = f"{contract_name}{self.compile_ctx.uuid}.py" # uncomment for debugging the python code: # with open(py_file, "w") as f: @@ -1128,13 +1129,14 @@ def _ensure_source_pos(ir_node, source_pos=None, error_msg=None): def executor_from_ir(ir_node, vyper_compiler_data) -> Any: _ensure_source_pos(ir_node) - ret = _executor_from_ir(ir_node, CompileContext(vyper_compiler_data)) + ctx = CompileContext(vyper_compiler_data) + ret = _executor_from_ir(ir_node, ctx) ret = ret.analyze() # TODO: rename this, this is "something.vy", but we maybe want # "something.py " - ret.compile_main(vyper_compiler_data.contract_path) + ret.compile_main(ctx.contract_name) return ret diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 1755223f..a22291a7 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -12,7 +12,6 @@ import vyper import vyper.ast as vy_ast import vyper.ir.compile_ir as compile_ir -import vyper.semantics.analysis as analysis import vyper.semantics.namespace as vy_ns from eth.exceptions import VMError from vyper.ast.parse import parse_to_ast @@ -29,8 +28,8 @@ from vyper.evm.opcodes import anchor_evm_version from vyper.exceptions import VyperException from vyper.ir.optimizer import optimize -from vyper.semantics.analysis.data_positions import set_data_positions -from vyper.semantics.types import AddressT, HashMapT, TupleT +from vyper.semantics.analysis.base import VarInfo +from vyper.semantics.types import AddressT, HashMapT, SelfT, TupleT from vyper.utils import method_id from boa import BoaError @@ -489,6 +488,11 @@ def __init__( continue setattr(self.internal, fn.name, VyperInternalFunction(fn, self)) + # TODO: set library methods as class.internal attributes? + + # not sure if this is accurate in the presence of modules + self._function_id = len(self.module_t.function_defs) + self._storage = StorageModel(self) self._eval_cache = lrudict(0x1000) @@ -727,17 +731,32 @@ def line_profile(self, computation=None): @cached_property def _ast_module(self): module = copy.deepcopy(self.compiler_data.annotated_vyper_module) + self._cache_namespace(module._metadata["namespace"]) # the global namespace is expensive to compute, so cache it def _cache_namespace(self, namespace): # copy.copy doesn't really work on Namespace objects, copy by hand ret = vy_ns.Namespace() - ret._scopes = copy.deepcopy(namespace._scopes) + ret._scopes = [set()] for s in namespace._scopes: + ret.append(set()) for n in s: ret[n] = namespace[n] + + # recreate the "self" object because it gets removed from the global + # namespace at the end of validate_semantics. do not try this at home! + self_t = SelfT() + for s, t in self.module_t.members.items(): + self_t.add_member(s, t) + ret["self"] = VarInfo(self_t) + ret._scopes[-1].remove("self") + ret._scopes[0].add("self") self._vyper_namespace = ret + def _get_function_id(self): + self._function_id += 1 + return self._function_id + @contextlib.contextmanager def override_vyper_namespace(self): # ensure self._vyper_namespace is computed @@ -789,7 +808,7 @@ def unoptimized_ir(self): with anchor_opt_level(OptimizationLevel.NONE), anchor_evm_version( self.compiler_data.settings.evm_version ): - return generate_ir_for_module(self.compiler_data.module_t) + return generate_ir_for_module(self.module_t) @cached_property def ir_executor(self): diff --git a/boa/interpret.py b/boa/interpret.py index d4ea8229..9217e1e7 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -5,8 +5,8 @@ import vyper from vyper.cli.vyper_compile import get_search_paths -from vyper.compiler.phases import CompilerData from vyper.compiler.input_bundle import FileInput, FilesystemInputBundle +from vyper.compiler.phases import CompilerData from boa.contracts.abi.abi_contract import ABIContractFactory from boa.contracts.vyper.compiler_utils import anchor_compiler_settings @@ -40,19 +40,26 @@ def set_search_path(path: list[str]): _search_path = path -def compiler_data(source_code: str, contract_name: str, filename: str, **kwargs) -> CompilerData: +def compiler_data( + source_code: str, contract_name: str, filename: str, **kwargs +) -> CompilerData: global _disk_cache, _search_path # TODO: figure out how caching works with modules. if True: - file_input = FileInput(source_code=source_code, source_id=-1, path=Path(contract_name), resolved_path=Path(contract_name)) + file_input = FileInput( + source_code=source_code, + source_id=-1, + path=Path(contract_name), + resolved_path=Path(contract_name), + ) search_paths = get_search_paths(_search_path) input_bundle = FilesystemInputBundle(search_paths) return CompilerData(file_input, input_bundle, **kwargs) def func(): raise Exception("unreachable") - ret = CompilerData(source_code, contract_name, interface_codes=ifaces, **kwargs) + ret = CompilerData(source_code, contract_name, **kwargs) with anchor_compiler_settings(ret): _ = ret.bytecode, ret.bytecode_runtime # force compilation to happen return ret From 3ee30333bfd769c670a38bb00ac555cb358a79c0 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 13 Feb 2024 16:28:53 -0500 Subject: [PATCH 05/48] fix remaining tests --- boa/contracts/vyper/vyper_contract.py | 2 +- tests/unitary/test_gas_profiling.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index a22291a7..185fcdd8 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -862,7 +862,7 @@ def inject_function(self, fn_source_code, force=False): # get an AST so we know the fn name; work is doubled in # _compile_vyper_function but no way around it. - fn_ast = parse_to_ast(fn_source_code, {}).body[0] + fn_ast = parse_to_ast(fn_source_code).body[0] if hasattr(self.inject, fn_ast.name) and not force: raise ValueError(f"already injected: {fn_ast.name}") diff --git a/tests/unitary/test_gas_profiling.py b/tests/unitary/test_gas_profiling.py index ed562606..d034ce3e 100644 --- a/tests/unitary/test_gas_profiling.py +++ b/tests/unitary/test_gas_profiling.py @@ -50,7 +50,7 @@ def variable_loop_contract(): @view def foo(a: uint256, b: uint256, c: uint256) -> uint256: d: uint256 = 0 - for j in range(1000): + for j: uint256 in range(1000): d = d + a + b if d > c: break @@ -60,7 +60,7 @@ def foo(a: uint256, b: uint256, c: uint256) -> uint256: @view def _barfoo(a: uint256, b: uint256, c: uint256) -> uint256: d: uint256 = 0 - for j in range(1000): + for j: uint256 in range(1000): d = d * a / b if d > c: break From 59b1f4bf7423b45843ffb73fa5e907411f9abfbc Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 13 Feb 2024 16:32:50 -0500 Subject: [PATCH 06/48] update more tests for v0.4.0 --- examples/ERC20.vy | 4 ++-- examples/deployer.vy | 2 +- tests/integration/RewardPool.vy | 14 +++++++------- tests/integration/fork/test_abi_contract.py | 2 +- tests/integration/veYFI.vy | 10 +++++----- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/examples/ERC20.vy b/examples/ERC20.vy index 521406c3..f8963472 100644 --- a/examples/ERC20.vy +++ b/examples/ERC20.vy @@ -2,8 +2,8 @@ # @author Takayuki Jimba (@yudetamago) # https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md -from vyper.interfaces import ERC20 -from vyper.interfaces import ERC20Detailed +from ethereum.ercs import ERC20 +from ethereum.ercs import ERC20Detailed implements: ERC20 implements: ERC20Detailed diff --git a/examples/deployer.vy b/examples/deployer.vy index 6f5ebaae..344869bb 100644 --- a/examples/deployer.vy +++ b/examples/deployer.vy @@ -1,4 +1,4 @@ -from vyper.interfaces import ERC20 +from ethereum.ercs import ERC20 BLUEPRINT: immutable(address) diff --git a/tests/integration/RewardPool.vy b/tests/integration/RewardPool.vy index e8f054dd..3a47521f 100644 --- a/tests/integration/RewardPool.vy +++ b/tests/integration/RewardPool.vy @@ -3,7 +3,7 @@ @author Curve Finance, Yearn Finance @license MIT """ -from vyper.interfaces import ERC20 +from ethereum.ercs import ERC20 interface VotingYFI: def user_point_epoch(addr: address) -> uint256: view @@ -93,7 +93,7 @@ def _checkpoint_token(): this_week: uint256 = t / WEEK * WEEK next_week: uint256 = 0 - for i in range(20): + for i: uint256 in range(20): next_week = this_week + WEEK if block.timestamp < next_week: if since_last == 0 and block.timestamp == t: @@ -129,7 +129,7 @@ def checkpoint_token(): def _find_timestamp_epoch(ve: address, _timestamp: uint256) -> uint256: _min: uint256 = 0 _max: uint256 = VEYFI.epoch() - for i in range(128): + for i: uint256 in range(128): if _min >= _max: break _mid: uint256 = (_min + _max + 2) / 2 @@ -146,7 +146,7 @@ def _find_timestamp_epoch(ve: address, _timestamp: uint256) -> uint256: def _find_timestamp_user_epoch(ve: address, user: address, _timestamp: uint256, max_user_epoch: uint256) -> uint256: _min: uint256 = 0 _max: uint256 = max_user_epoch - for i in range(128): + for i: uint256 in range(128): if _min >= _max: break _mid: uint256 = (_min + _max + 2) / 2 @@ -180,7 +180,7 @@ def _checkpoint_total_supply(): rounded_timestamp: uint256 = block.timestamp / WEEK * WEEK VEYFI.checkpoint() - for i in range(20): + for i: uint256 in range(20): if t > rounded_timestamp: break else: @@ -245,7 +245,7 @@ def _claim(addr: address, last_token_time: uint256) -> uint256: old_user_point: Point = empty(Point) # Iterate over weeks - for i in range(50): + for i: uint256 in range(50): if week_cursor >= last_token_time: break @@ -280,7 +280,7 @@ def _claim(addr: address, last_token_time: uint256) -> uint256: @external -@nonreentrant('lock') +@nonreentrant def claim(user: address = msg.sender, relock: bool = False) -> uint256: """ @notice Claim fees for a user diff --git a/tests/integration/fork/test_abi_contract.py b/tests/integration/fork/test_abi_contract.py index 9085a7ae..4183febf 100644 --- a/tests/integration/fork/test_abi_contract.py +++ b/tests/integration/fork/test_abi_contract.py @@ -122,7 +122,7 @@ def test_fork_write(crvusd, n): def test_abi_stack_trace(crvusd): c = boa.loads( """ -from vyper.interfaces import ERC20 +from ethereum.ercs import ERC20 @external def foo(x: ERC20, from_: address): x.transferFrom(from_, self, 100) diff --git a/tests/integration/veYFI.vy b/tests/integration/veYFI.vy index b98b0a86..46bcc2a7 100644 --- a/tests/integration/veYFI.vy +++ b/tests/integration/veYFI.vy @@ -10,7 +10,7 @@ Vote weight decays linearly over time. A user can unlock funds early incurring a penalty. """ -from vyper.interfaces import ERC20 +from ethereum.ercs import ERC20 interface RewardPool: def burn(amount: uint256) -> bool: nonpayable @@ -182,7 +182,7 @@ def _checkpoint_global() -> Point: # apply weekly slope changes and record weekly global snapshots t_i: uint256 = self.round_to_week(last_checkpoint) - for i in range(255): + for i: uint256 in range(255): t_i = min(t_i + WEEK, block.timestamp) last_point.bias -= last_point.slope * convert(t_i - last_checkpoint, int128) last_point.slope += self.slope_changes[self][t_i] # will read 0 if not aligned to week @@ -347,7 +347,7 @@ def find_epoch_by_block(user: address, height: uint256, max_epoch: uint256) -> u """ _min: uint256 = 0 _max: uint256 = max_epoch - for i in range(128): # Will be always enough for 128-bit numbers + for i: uint256 in range(128): # Will be always enough for 128-bit numbers if _min >= _max: break _mid: uint256 = (_min + _max + 1) / 2 @@ -369,7 +369,7 @@ def find_epoch_by_timestamp(user: address, ts: uint256, max_epoch: uint256) -> u """ _min: uint256 = 0 _max: uint256 = max_epoch - for i in range(128): # Will be always enough for 128-bit numbers + for i: uint256 in range(128): # Will be always enough for 128-bit numbers if _min >= _max: break _mid: uint256 = (_min + _max + 1) / 2 @@ -386,7 +386,7 @@ def replay_slope_changes(user: address, point: Point, ts: uint256) -> Point: upoint: Point = point t_i: uint256 = self.round_to_week(upoint.ts) - for i in range(500): + for i: uint256 in range(500): t_i += WEEK d_slope: int128 = 0 if t_i > ts: From da574d8ba8878b2dceb46985d9a826be0e858c6e Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 21 Feb 2024 12:57:06 -0500 Subject: [PATCH 07/48] fix constants requires updates to vyper: Namespace.copy --- boa/contracts/vyper/compiler_utils.py | 2 +- boa/contracts/vyper/vyper_contract.py | 36 ++++++--------------------- 2 files changed, 8 insertions(+), 30 deletions(-) diff --git a/boa/contracts/vyper/compiler_utils.py b/boa/contracts/vyper/compiler_utils.py index e37aec31..b493d17d 100644 --- a/boa/contracts/vyper/compiler_utils.py +++ b/boa/contracts/vyper/compiler_utils.py @@ -135,7 +135,7 @@ def generate_bytecode_for_arbitrary_stmt(source_code, contract): """Wraps arbitrary stmts with external fn and generates bytecode""" ast = parse_to_ast(source_code) - vy_ast.folding.fold(ast) + ast = ast.body[0] return_sig = "" diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 2c86cbb7..e83f2714 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -28,8 +28,7 @@ from vyper.evm.opcodes import anchor_evm_version from vyper.exceptions import VyperException from vyper.ir.optimizer import optimize -from vyper.semantics.analysis.base import VarInfo -from vyper.semantics.types import AddressT, HashMapT, SelfT, TupleT +from vyper.semantics.types import AddressT, HashMapT, TupleT from vyper.utils import method_id from boa import BoaError @@ -729,39 +728,19 @@ def line_profile(self, computation=None): ret.merge(child_obj.line_profile(child)) return ret - @cached_property - def _ast_module(self): - module = copy.deepcopy(self.compiler_data.annotated_vyper_module) - self._cache_namespace(module._metadata["namespace"]) - - # the global namespace is expensive to compute, so cache it - def _cache_namespace(self, namespace): - # copy.copy doesn't really work on Namespace objects, copy by hand - ret = vy_ns.Namespace() - ret._scopes = [set()] - for s in namespace._scopes: - ret.append(set()) - for n in s: - ret[n] = namespace[n] - - # recreate the "self" object because it gets removed from the global - # namespace at the end of validate_semantics. do not try this at home! - self_t = SelfT() - for s, t in self.module_t.members.items(): - self_t.add_member(s, t) - ret["self"] = VarInfo(self_t) - ret._scopes[-1].remove("self") - ret._scopes[0].add("self") - self._vyper_namespace = ret - def _get_function_id(self): self._function_id += 1 return self._function_id + @cached_property + def _vyper_namespace(self): + module = self.compiler_data.annotated_vyper_module + # make a copy of the namespace, since we might modify it + return copy.copy(module._metadata["namespace"]) + @contextlib.contextmanager def override_vyper_namespace(self): # ensure self._vyper_namespace is computed - m = self._ast_module # noqa: F841 contract_members = self._vyper_namespace["self"].typ.members try: to_keep = set(contract_members.keys()) @@ -868,7 +847,6 @@ def inject_function(self, fn_source_code, force=False): raise ValueError(f"already injected: {fn_ast.name}") # ensure self._vyper_namespace is computed - m = self._ast_module # noqa: F841 self._vyper_namespace["self"].typ.members.pop(fn_ast.name, None) f = _InjectVyperFunction(self, fn_source_code) setattr(self.inject, fn_ast.name, f) From 7fe14fd065192ec947f82b6969497894a01910ab Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 27 Feb 2024 20:27:05 -0500 Subject: [PATCH 08/48] fix mypy --- boa/interpret.py | 1 + 1 file changed, 1 insertion(+) diff --git a/boa/interpret.py b/boa/interpret.py index f8e06310..7d6df773 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -152,6 +152,7 @@ def loads_partial( compiler_args: dict = None, ) -> VyperDeployer: name = name or "VyperContract" # TODO handle this upstream in CompilerData + filename = filename or "" if dedent: source_code = textwrap.dedent(source_code) From 6308144eef4f1fbaf6b4c21328bda1a7729b6b92 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 27 Feb 2024 20:27:08 -0500 Subject: [PATCH 09/48] updates for some API changes --- boa/contracts/vyper/compiler_utils.py | 11 +++++------ boa/contracts/vyper/vyper_contract.py | 7 ++++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/boa/contracts/vyper/compiler_utils.py b/boa/contracts/vyper/compiler_utils.py index b493d17d..79a777f7 100644 --- a/boa/contracts/vyper/compiler_utils.py +++ b/boa/contracts/vyper/compiler_utils.py @@ -10,7 +10,7 @@ generate_ir_for_internal_function, ) from vyper.codegen.ir_node import IRnode -from vyper.codegen.module import _globally_reachable_functions +from vyper.codegen.module import _runtime_reachable_functions from vyper.evm.opcodes import anchor_evm_version from vyper.exceptions import InvalidType from vyper.ir import compile_ir, optimizer @@ -50,8 +50,7 @@ def compile_vyper_function(vyper_function, contract): ast = ast.body[0] func_t = ast._metadata["func_type"] - if func_t._function_id is None: - func_t._function_id = contract._get_function_id() + contract.ensure_id(func_t) funcinfo = generate_ir_for_external_function(ast, module_t) ir = funcinfo.common_ir @@ -68,13 +67,13 @@ def compile_vyper_function(vyper_function, contract): _, contract_runtime = contract.unoptimized_ir ir_list = ["seq", ir, contract_runtime] reachable = func_t.reachable_internal_functions - already_compiled = _globally_reachable_functions(module_t.function_defs) + + already_compiled = _runtime_reachable_functions(module_t, contract) missing_functions = reachable.difference(already_compiled) # TODO: cache function compilations or something for f in missing_functions: assert f.ast_def is not None - if f._function_id is None: - f._function_id = contract._get_function_id() + contract.ensure_id(f) ir_list.append( generate_ir_for_internal_function(f.ast_def, module_t, False).func_ir ) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index c37fb59e..ac7c0e19 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -741,9 +741,10 @@ def line_profile(self, computation=None): ret.merge(child_obj.line_profile(child)) return ret - def _get_function_id(self): - self._function_id += 1 - return self._function_id + def ensure_id(self, fn_t): # mimic vyper.codegen.module.IDGenerator api + if fn_t._function_id is None: + fn_t._function_id = self._function_id + self._function_id += 1 @cached_property def _vyper_namespace(self): From 426ac8c84da64cac5e5f5e7d463e5962232cc802 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 27 Feb 2024 20:33:52 -0500 Subject: [PATCH 10/48] update a test --- tests/unitary/test_abi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unitary/test_abi.py b/tests/unitary/test_abi.py index 6d079898..cdcf87c4 100644 --- a/tests/unitary/test_abi.py +++ b/tests/unitary/test_abi.py @@ -99,8 +99,8 @@ def test_address_nested(): @view def test(_a: DynArray[uint256, 100]) -> ((DynArray[Test, 2], uint256), uint256): first: DynArray[Test, 2] = [ - Test({address: msg.sender, number: _a[0]}), - Test({address: msg.sender, number: _a[1]}), + Test(address=msg.sender, number=_a[0]), + Test(address=msg.sender, number=_a[1]), ] return (first, _a[2]), _a[3] """ From 99a2d8ee18d973fd068c2561e944e7f38d9bf3ae Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 27 Feb 2024 20:34:42 -0500 Subject: [PATCH 11/48] update examples --- examples/ERC20.vy | 8 ++++---- examples/deployer.vy | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/ERC20.vy b/examples/ERC20.vy index f8963472..1d309763 100644 --- a/examples/ERC20.vy +++ b/examples/ERC20.vy @@ -2,11 +2,11 @@ # @author Takayuki Jimba (@yudetamago) # https://github.com/ethereum/EIPs/blob/master/EIPS/eip-20.md -from ethereum.ercs import ERC20 -from ethereum.ercs import ERC20Detailed +from ethereum.ercs import IERC20 +from ethereum.ercs import IERC20Detailed -implements: ERC20 -implements: ERC20Detailed +implements: IERC20 +implements: IERC20Detailed event Transfer: sender: indexed(address) diff --git a/examples/deployer.vy b/examples/deployer.vy index 344869bb..53d91f0b 100644 --- a/examples/deployer.vy +++ b/examples/deployer.vy @@ -1,4 +1,4 @@ -from ethereum.ercs import ERC20 +from ethereum.ercs import IERC20 BLUEPRINT: immutable(address) @@ -7,6 +7,6 @@ def __init__(blueprint_address: address): BLUEPRINT = blueprint_address @external -def create_new_erc20(name: String[32], symbol: String[32], decimals: uint8, supply: uint256) -> ERC20: +def create_new_erc20(name: String[32], symbol: String[32], decimals: uint8, supply: uint256) -> IERC20: t: address = create_from_blueprint(BLUEPRINT, name, symbol, decimals, supply, code_offset=3) - return ERC20(t) + return IERC20(t) From 00772eec2e0f1c31950656a117185a0877b3afa2 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 28 Feb 2024 07:19:09 -0500 Subject: [PATCH 12/48] fixup function name --- boa/contracts/vyper/compiler_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boa/contracts/vyper/compiler_utils.py b/boa/contracts/vyper/compiler_utils.py index 79a777f7..29772d4a 100644 --- a/boa/contracts/vyper/compiler_utils.py +++ b/boa/contracts/vyper/compiler_utils.py @@ -45,7 +45,7 @@ def compile_vyper_function(vyper_function, contract): # override namespace and add wrapper code at the top with contract.override_vyper_namespace(): - analysis.validate_semantics(ast, compiler_data.input_bundle) + analysis.analyze_module(ast, compiler_data.input_bundle) ast = ast.body[0] func_t = ast._metadata["func_type"] From 7770c990a4c9f162e605a0761a9fe2af4c9385cc Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 28 Feb 2024 07:19:21 -0500 Subject: [PATCH 13/48] workaround for namespace scopes --- boa/contracts/vyper/vyper_contract.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index ac7c0e19..baf6a1d1 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -750,7 +750,12 @@ def ensure_id(self, fn_t): # mimic vyper.codegen.module.IDGenerator api def _vyper_namespace(self): module = self.compiler_data.annotated_vyper_module # make a copy of the namespace, since we might modify it - return copy.copy(module._metadata["namespace"]) + ret = copy.copy(module._metadata["namespace"]) + ret._scopes = copy.deepcopy(ret._scopes) + if len(ret._scopes) == 0: + # funky behavior in Namespace.enter_scope() + ret._scopes.append(set()) + return ret @contextlib.contextmanager def override_vyper_namespace(self): From 80d6e0cbd0b80ae4175e0d6750cf02853fcd48c1 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 28 Feb 2024 07:32:30 -0500 Subject: [PATCH 14/48] update exposed_functions logic --- boa/contracts/vyper/vyper_contract.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index baf6a1d1..f6632041 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -475,15 +475,16 @@ def __init__( # add all exposed functions from the interface to the contract exposed_fns = { - fn.name: fn - for fn in self.module_t.function_defs - if not fn._metadata["func_type"].is_internal + fn_t.name: fn_t.decl_node + for fn_t in compiler_data.global_ctx.exposed_functions } # set external methods as class attributes: self._ctor = None - if "__init__" in exposed_fns: - self._ctor = VyperFunction(exposed_fns.pop("__init__"), self) + if compiler_data.global_ctx.init_function is not None: + self._ctor = VyperFunction( + compiler_data.global_ctx.init_function.decl_node, self + ) if skip_initcode: addr = Address(override_address) From 2905c92d71480088cb37e6f39c71511863191ba3 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 28 Feb 2024 07:32:39 -0500 Subject: [PATCH 15/48] test updates --- boa/profiling.py | 2 +- tests/unitary/test_coverage.py | 2 +- tests/unitary/test_gas_profiling.py | 2 +- tests/unitary/test_import_overloading.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/boa/profiling.py b/boa/profiling.py index 4afea4fa..6152f55d 100644 --- a/boa/profiling.py +++ b/boa/profiling.py @@ -334,7 +334,7 @@ def get_call_profile_table(env: Env) -> Table: cname = "" caddr = "" if c == 0: - cname = profile.contract_path + cname = str(profile.contract_path) caddr = address fn_name = profile.fn_name table.add_row(cname, caddr, fn_name, *stats.net_gas_stats.get_str_repr()) diff --git a/tests/unitary/test_coverage.py b/tests/unitary/test_coverage.py index 53912b32..e7954b07 100644 --- a/tests/unitary/test_coverage.py +++ b/tests/unitary/test_coverage.py @@ -22,7 +22,7 @@ def foo(a: uint256) -> uint256: view FOO: immutable(address) -@external +@deploy def __init__(_foo_address: address): FOO = _foo_address diff --git a/tests/unitary/test_gas_profiling.py b/tests/unitary/test_gas_profiling.py index d034ce3e..296b7189 100644 --- a/tests/unitary/test_gas_profiling.py +++ b/tests/unitary/test_gas_profiling.py @@ -61,7 +61,7 @@ def foo(a: uint256, b: uint256, c: uint256) -> uint256: def _barfoo(a: uint256, b: uint256, c: uint256) -> uint256: d: uint256 = 0 for j: uint256 in range(1000): - d = d * a / b + d = (d * a) // b if d > c: break return d diff --git a/tests/unitary/test_import_overloading.py b/tests/unitary/test_import_overloading.py index c4664cbc..e9354e4c 100644 --- a/tests/unitary/test_import_overloading.py +++ b/tests/unitary/test_import_overloading.py @@ -30,7 +30,7 @@ def test_imports(tmp_path): code = """ totalSupply: public(uint256) -@external +@deploy def __init__(initial_supply: uint256): self.totalSupply = initial_supply """ From bcc7d9182c6cfb5736df00032a09249e3d119914 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 7 Mar 2024 04:37:14 -0800 Subject: [PATCH 16/48] fix for transient variables --- boa/contracts/vyper/vyper_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 5d0c814e..8c01c62f 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -425,7 +425,7 @@ def __init__(self, contract): compiler_data = contract.compiler_data # TODO: recurse into imported modules for k, v in contract.module_t.variables.items(): - is_storage = not v.is_immutable and not v.is_constant + is_storage = not (v.is_immutable or v.is_constant or v.is_transient) if is_storage: slot = compiler_data.storage_layout["storage_layout"][k]["slot"] setattr(self, k, StorageVar(contract, slot, v.typ)) From 9cf289ff06b1dc6052f682caa32ab68a8669f879 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 9 Mar 2024 14:36:01 -0500 Subject: [PATCH 17/48] fix caching for modules --- boa/interpret.py | 81 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/boa/interpret.py b/boa/interpret.py index 28b7250e..370f8292 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -5,12 +5,19 @@ from importlib.machinery import SourceFileLoader from importlib.util import spec_from_loader from pathlib import Path -from typing import Any, Union +from typing import TYPE_CHECKING, Any, Union import vyper from vyper.cli.vyper_compile import get_search_paths -from vyper.compiler.input_bundle import FileInput, FilesystemInputBundle +from vyper.compiler.input_bundle import ( + ABIInput, + CompilerInput, + FileInput, + FilesystemInputBundle, +) from vyper.compiler.phases import CompilerData +from vyper.semantics.types.module import ModuleT +from vyper.utils import sha256sum from boa.contracts.abi.abi_contract import ABIContractFactory from boa.contracts.vyper.compiler_utils import anchor_compiler_settings @@ -23,6 +30,9 @@ from boa.util.abi import Address from boa.util.disk_cache import DiskCache +if TYPE_CHECKING: + from vyper.semantics.analysis.base import ImportInfo + _Contract = Union[VyperContract, VyperBlueprint] @@ -82,31 +92,66 @@ def create_module(self, spec): sys.meta_path.append(BoaImporter()) +def hash_input(compiler_input: CompilerInput) -> str: + if isinstance(compiler_input, FileInput): + return compiler_input.sha256sum + if isinstance(compiler_input, ABIInput): + return sha256sum(str(compiler_input.abi)) + raise RuntimeError(f"bad compiler input {compiler_input}") + + +# compute a fingerprint for a module which changes if any of its +# dependencies change +# TODO consider putting this in its own module +def get_module_fingerprint( + module_t: ModuleT, seen: dict["ImportInfo", str] = None +) -> str: + seen = seen or {} + fingerprints = [] + for stmt in module_t.import_stmts: + import_info = stmt._metdata["import_info"] + if import_info not in seen: + if isinstance(import_info.typ, ModuleT): + fingerprint = get_module_fingerprint(import_info.typ, seen) + else: + fingerprint = hash_input(import_info.compiler_input) + seen[import_info] = fingerprint + fingerprint = seen[import_info] + fingerprints.append(fingerprint) + fingerprints.append(module_t._module.source_sha256sum) + + return sha256sum("".join(fingerprints)) + + def compiler_data( source_code: str, contract_name: str, filename: str, **kwargs ) -> CompilerData: global _disk_cache, _search_path - # TODO: figure out how caching works with modules. - if True: - file_input = FileInput( - source_code=source_code, - source_id=-1, - path=Path(contract_name), - resolved_path=Path(contract_name), - ) - search_paths = get_search_paths(_search_path) - input_bundle = FilesystemInputBundle(search_paths) - return CompilerData(file_input, input_bundle, **kwargs) + file_input = FileInput( + source_code=source_code, + source_id=-1, + path=Path(contract_name), + resolved_path=Path(filename), + ) + search_paths = get_search_paths(_search_path) + input_bundle = FilesystemInputBundle(search_paths) + + ret = CompilerData(file_input, input_bundle, **kwargs) + if _disk_cache is None: + return ret + + with anchor_compiler_settings(ret): + module_t = ret.annotated_vyper_module._metadata["type"] + fingerprint = get_module_fingerprint(module_t) - def func(): - raise Exception("unreachable") - ret = CompilerData(source_code, contract_name, **kwargs) + def get_compiler_data(): with anchor_compiler_settings(ret): - _ = ret.bytecode, ret.bytecode_runtime # force compilation to happen + # force compilation to happen so DiskCache will cache the compiled artifact: + _ = ret.bytecode, ret.bytecode_runtime return ret - return _disk_cache.caching_lookup(str((kwargs, source_code)), func) + return _disk_cache.caching_lookup(str((kwargs, fingerprint)), get_compiler_data) def load(filename: str, *args, **kwargs) -> _Contract: # type: ignore From e6c560b34976ab54df5a41994aae508e2dbbc933 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 9 Mar 2024 14:37:55 -0500 Subject: [PATCH 18/48] fix typos --- boa/interpret.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/boa/interpret.py b/boa/interpret.py index 370f8292..33b698ee 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -109,14 +109,14 @@ def get_module_fingerprint( seen = seen or {} fingerprints = [] for stmt in module_t.import_stmts: - import_info = stmt._metdata["import_info"] - if import_info not in seen: + import_info = stmt._metadata["import_info"] + if id(import_info) not in seen: if isinstance(import_info.typ, ModuleT): fingerprint = get_module_fingerprint(import_info.typ, seen) else: fingerprint = hash_input(import_info.compiler_input) - seen[import_info] = fingerprint - fingerprint = seen[import_info] + seen[id(import_info)] = fingerprint + fingerprint = seen[id(import_info)] fingerprints.append(fingerprint) fingerprints.append(module_t._module.source_sha256sum) From ac2174467da1d55b680ae031d6dd6f6ef28407d9 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 9 Mar 2024 15:32:10 -0500 Subject: [PATCH 19/48] add a note --- boa/interpret.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/boa/interpret.py b/boa/interpret.py index 33b698ee..f980031f 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -142,6 +142,10 @@ def compiler_data( return ret with anchor_compiler_settings(ret): + # note that this actually parses and analyzes all dependencies, + # even if they haven't changed. an optimization would be to + # somehow convince vyper (in ModuleAnalyzer) to get the module_t + # from the cache. module_t = ret.annotated_vyper_module._metadata["type"] fingerprint = get_module_fingerprint(module_t) From 038f8d802898644be61b08d6d0ff08108228e6f5 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 14 Mar 2024 09:11:20 -0400 Subject: [PATCH 20/48] update an import for latest vyper commit --- boa/contracts/vyper/ast_utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/boa/contracts/vyper/ast_utils.py b/boa/contracts/vyper/ast_utils.py index 1a4b09f0..d662de0d 100644 --- a/boa/contracts/vyper/ast_utils.py +++ b/boa/contracts/vyper/ast_utils.py @@ -4,7 +4,7 @@ from typing import Any, Optional import vyper.ast as vy_ast -from vyper.codegen.core import getpos +from vyper.ir.compile_ir import getpos def get_block(source_code: str, lineno: int, end_lineno: int) -> str: @@ -45,6 +45,8 @@ def reason_at( # build a reverse map from the format we have in pc_pos_map to AST nodes +# (TODO: we might not need this anymore since vyper exports map from pc +# to ast node directly as of 0.4.0) def ast_map_of(ast_node): ast_map = {} nodes = [ast_node] + ast_node.get_descendants(reverse=True) From 52b0f73bc2101313e4c23ca827b8f335a4e17917 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sun, 14 Apr 2024 14:45:01 -0400 Subject: [PATCH 21/48] update anchor_settings usages --- boa/contracts/vyper/compiler_utils.py | 13 ++----------- boa/contracts/vyper/vyper_contract.py | 24 +++++++++++------------- boa/interpret.py | 6 +++--- tests/unitary/test_deploy_value.py | 2 +- tests/unitary/test_fixture_order.py | 7 +++++-- 5 files changed, 22 insertions(+), 30 deletions(-) diff --git a/boa/contracts/vyper/compiler_utils.py b/boa/contracts/vyper/compiler_utils.py index 29772d4a..5d145ec8 100644 --- a/boa/contracts/vyper/compiler_utils.py +++ b/boa/contracts/vyper/compiler_utils.py @@ -1,17 +1,15 @@ -import contextlib import textwrap import vyper.ast as vy_ast import vyper.semantics.analysis as analysis from vyper.ast.parse import parse_to_ast -from vyper.codegen.core import anchor_opt_level from vyper.codegen.function_definitions import ( generate_ir_for_external_function, generate_ir_for_internal_function, ) from vyper.codegen.ir_node import IRnode from vyper.codegen.module import _runtime_reachable_functions -from vyper.evm.opcodes import anchor_evm_version +from vyper.compiler.settings import anchor_settings from vyper.exceptions import InvalidType from vyper.ir import compile_ir, optimizer from vyper.semantics.analysis.utils import get_exact_type_from_node @@ -22,13 +20,6 @@ _METHOD_ID_VAR = "_calldata_method_id" -@contextlib.contextmanager -def anchor_compiler_settings(compiler_data): - settings = compiler_data.settings - with anchor_opt_level(settings.optimize), anchor_evm_version(settings.evm_version): - yield - - def compile_vyper_function(vyper_function, contract): """Compiles a vyper function and appends it to the top of the IR of a contract. This is useful for vyper `eval` and internal functions, where @@ -39,7 +30,7 @@ def compile_vyper_function(vyper_function, contract): compiler_data = contract.compiler_data - with anchor_compiler_settings(compiler_data): + with anchor_settings(compiler_data.settings): module_t = contract.module_t ast = parse_to_ast(vyper_function) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 0df0ce99..026e4b04 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -15,7 +15,7 @@ import vyper.semantics.namespace as vy_ns from eth.exceptions import VMError from vyper.ast.parse import parse_to_ast -from vyper.codegen.core import anchor_opt_level, calculate_type_for_external_return +from vyper.codegen.core import calculate_type_for_external_return from vyper.codegen.function_definitions import ( generate_ir_for_external_function, generate_ir_for_internal_function, @@ -25,8 +25,7 @@ from vyper.compiler import CompilerData from vyper.compiler import output as compiler_output from vyper.compiler.output import build_abi_output -from vyper.compiler.settings import OptimizationLevel -from vyper.evm.opcodes import anchor_evm_version +from vyper.compiler.settings import OptimizationLevel, anchor_settings from vyper.exceptions import VyperException from vyper.ir.optimizer import optimize from vyper.semantics.types import AddressT, HashMapT, TupleT @@ -45,7 +44,6 @@ ) from boa.contracts.vyper.compiler_utils import ( _METHOD_ID_VAR, - anchor_compiler_settings, compile_vyper_function, generate_bytecode_for_arbitrary_stmt, generate_bytecode_for_internal_fn, @@ -78,7 +76,7 @@ def __init__(self, compiler_data, filename=None): # force compilation so that if there are any errors in the contract, # we fail at load rather than at deploy time. - with anchor_compiler_settings(self.compiler_data): + with anchor_settings(self.compiler_data.settings): _ = compiler_data.bytecode, compiler_data.bytecode_runtime self.filename = filename @@ -140,7 +138,7 @@ def __init__( super().__init__(env, filename) self.compiler_data = compiler_data - with anchor_compiler_settings(self.compiler_data): + with anchor_settings(self.compiler_data.settings): _ = compiler_data.bytecode, compiler_data.bytecode_runtime @cached_property @@ -615,7 +613,7 @@ def module_t(self): @property def source_map(self): if self._source_map is None: - with anchor_compiler_settings(self.compiler_data): + with anchor_settings(self.compiler_data.settings): _, self._source_map = compile_ir.assembly_to_evm( self.compiler_data.assembly_runtime ) @@ -785,7 +783,7 @@ def override_vyper_namespace(self): # eliminator might prune a dead function (which we want to eval) @cached_property def unoptimized_assembly(self): - with anchor_evm_version(self.compiler_data.settings.evm_version): + with anchor_settings(self.compiler_data.settings): runtime = self.unoptimized_ir[1] return compile_ir.compile_to_assembly( runtime, optimize=OptimizationLevel.NONE @@ -805,7 +803,7 @@ def data_section(self): @cached_property def unoptimized_bytecode(self): - with anchor_evm_version(self.compiler_data.settings.evm_version): + with anchor_settings(self.compiler_data.settings): s, _ = compile_ir.assembly_to_evm( self.unoptimized_assembly, insert_vyper_signature=True ) @@ -813,15 +811,15 @@ def unoptimized_bytecode(self): @cached_property def unoptimized_ir(self): - with anchor_opt_level(OptimizationLevel.NONE), anchor_evm_version( - self.compiler_data.settings.evm_version - ): + settings = self.compiler_data.settings.copy() + settings.optimize = OptimizationLevel.NONE + with anchor_settings(settings): return generate_ir_for_module(self.module_t) @cached_property def ir_executor(self): _, ir_runtime = self.unoptimized_ir - with anchor_evm_version(self.compiler_data.settings.evm_version): + with anchor_settings(self.compiler_data.settings): return executor_from_ir(ir_runtime, self.compiler_data) @contextlib.contextmanager diff --git a/boa/interpret.py b/boa/interpret.py index f980031f..c93e8669 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -16,11 +16,11 @@ FilesystemInputBundle, ) from vyper.compiler.phases import CompilerData +from vyper.compiler.settings import anchor_settings from vyper.semantics.types.module import ModuleT from vyper.utils import sha256sum from boa.contracts.abi.abi_contract import ABIContractFactory -from boa.contracts.vyper.compiler_utils import anchor_compiler_settings from boa.contracts.vyper.vyper_contract import ( VyperBlueprint, VyperContract, @@ -141,7 +141,7 @@ def compiler_data( if _disk_cache is None: return ret - with anchor_compiler_settings(ret): + with anchor_settings(ret.settings): # note that this actually parses and analyzes all dependencies, # even if they haven't changed. an optimization would be to # somehow convince vyper (in ModuleAnalyzer) to get the module_t @@ -150,7 +150,7 @@ def compiler_data( fingerprint = get_module_fingerprint(module_t) def get_compiler_data(): - with anchor_compiler_settings(ret): + with anchor_settings(ret.settings): # force compilation to happen so DiskCache will cache the compiled artifact: _ = ret.bytecode, ret.bytecode_runtime return ret diff --git a/tests/unitary/test_deploy_value.py b/tests/unitary/test_deploy_value.py index a5cf7bc5..34c03afc 100644 --- a/tests/unitary/test_deploy_value.py +++ b/tests/unitary/test_deploy_value.py @@ -1,4 +1,3 @@ -import eth.exceptions import pytest import boa @@ -21,6 +20,7 @@ def test_deploy_with_endowment(): assert boa.env.get_balance(boa.env.eoa) == 0 assert boa.env.get_balance(c.address) == 1000 + # try to call ctor with skip_init=True - must raise def test_deploy_with_endowment_must_init(): deployer = boa.loads_partial(fund_me_source) diff --git a/tests/unitary/test_fixture_order.py b/tests/unitary/test_fixture_order.py index 6540f512..ac4efba5 100644 --- a/tests/unitary/test_fixture_order.py +++ b/tests/unitary/test_fixture_order.py @@ -6,20 +6,23 @@ # eth_utils.exceptions.ValidationError: No checkpoint 31 was found -@pytest.fixture(scope='module', autouse=True, params=[7,8,9]) +@pytest.fixture(scope="module", autouse=True, params=[7, 8, 9]) def fixture_autouse(request): print(f"SETUP FIXTURE AUTOUSE {request.param}") yield print(f"TEARDOWN FIXTURE AUTOUSE {request.param}") -@pytest.fixture(scope='module', params=[1,2,3]) + +@pytest.fixture(scope="module", params=[1, 2, 3]) def fixture_test(request): print(f"SETUP FIXTURE TEST {request.param}") yield print(f"TEARDOWN FIXTURE TEST {request.param}") + def test_1(fixture_test): pass + def test_2(): pass From 2b4cd34dbbaf33ae65579175f8b01e5fcb7a653a Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sun, 14 Apr 2024 14:53:09 -0400 Subject: [PATCH 22/48] pass settings instead of compiler_args --- boa/interpret.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/boa/interpret.py b/boa/interpret.py index c93e8669..46c550f3 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -16,7 +16,7 @@ FilesystemInputBundle, ) from vyper.compiler.phases import CompilerData -from vyper.compiler.settings import anchor_settings +from vyper.compiler.settings import anchor_settings, Settings from vyper.semantics.types.module import ModuleT from vyper.utils import sha256sum @@ -137,7 +137,8 @@ def compiler_data( search_paths = get_search_paths(_search_path) input_bundle = FilesystemInputBundle(search_paths) - ret = CompilerData(file_input, input_bundle, **kwargs) + settings = Settings(**kwargs) + ret = CompilerData(file_input, input_bundle, settings) if _disk_cache is None: return ret From 50ac0fa9f9b3744c283eb9e21447e8c1b83ccb1e Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 16 Apr 2024 10:24:19 -0400 Subject: [PATCH 23/48] fix source map pc_pos_map is no longer in source map by default, upgrade to the new pc_raw_ast_map. --- boa/contracts/vyper/ast_utils.py | 12 ------------ boa/contracts/vyper/vyper_contract.py | 17 +++++------------ boa/coverage.py | 18 ++++++++---------- boa/interpret.py | 2 +- boa/profiling.py | 7 ++++--- 5 files changed, 18 insertions(+), 38 deletions(-) diff --git a/boa/contracts/vyper/ast_utils.py b/boa/contracts/vyper/ast_utils.py index d662de0d..0313fe80 100644 --- a/boa/contracts/vyper/ast_utils.py +++ b/boa/contracts/vyper/ast_utils.py @@ -4,7 +4,6 @@ from typing import Any, Optional import vyper.ast as vy_ast -from vyper.ir.compile_ir import getpos def get_block(source_code: str, lineno: int, end_lineno: int) -> str: @@ -44,17 +43,6 @@ def reason_at( return None -# build a reverse map from the format we have in pc_pos_map to AST nodes -# (TODO: we might not need this anymore since vyper exports map from pc -# to ast node directly as of 0.4.0) -def ast_map_of(ast_node): - ast_map = {} - nodes = [ast_node] + ast_node.get_descendants(reverse=True) - for node in nodes: - ast_map[getpos(node)] = node - return ast_map - - def get_fn_name_from_lineno(ast_map: dict, lineno: int) -> str: # TODO: this could be a performance bottleneck for source_map, node in ast_map.items(): diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 026e4b04..bc89a1da 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -37,11 +37,7 @@ _BaseEVMContract, _handle_child_trace, ) -from boa.contracts.vyper.ast_utils import ( - ast_map_of, - get_fn_ancestor_from_node, - reason_at, -) +from boa.contracts.vyper.ast_utils import get_fn_ancestor_from_node, reason_at from boa.contracts.vyper.compiler_utils import ( _METHOD_ID_VAR, compile_vyper_function, @@ -573,10 +569,6 @@ def deployer(self): def at(self, address): return self.deployer.at(address) - @cached_property - def ast_map(self): - return ast_map_of(self.compiler_data.vyper_module) - def _get_fn_from_computation(self, computation): node = self.find_source_of(computation) return get_fn_ancestor_from_node(node) @@ -610,6 +602,7 @@ def debug_frame(self, computation=None): def module_t(self): return self.compiler_data.global_ctx + # TODO: maybe rename to `ast_map` @property def source_map(self): if self._source_map is None: @@ -637,10 +630,10 @@ def find_source_of(self, computation, is_initcode=False): return self.ast_map.get(computation.vyper_source_pos) code_stream = computation.code - pc_map = self.source_map["pc_pos_map"] + ast_map = self.source_map["pc_raw_ast_map"] for pc in reversed(code_stream._trace): - if pc in pc_map and pc_map[pc] in self.ast_map: - return self.ast_map[pc_map[pc]] + if pc in ast_map: + return ast_map[pc] return None # ## handling events diff --git a/boa/coverage.py b/boa/coverage.py index d77b0316..e3453c53 100644 --- a/boa/coverage.py +++ b/boa/coverage.py @@ -82,14 +82,15 @@ def line_number_range(self, frame): if (pc := frame.f_locals.get("_pc")) is None: return (-1, -1) - pc_map = contract.source_map["pc_pos_map"] + pc_map = contract.source_map["pc_raw_ast_map"] - info = pc_map.get(pc) - if info is None: + node = pc_map.get(pc) + if node is None: return (-1, -1) - (start_lineno, _, end_lineno, _) = info - # `return start_lineno, end_lineno` doesn't seem to work. + start_lineno = node.lineno + # end_lineno = node.end_lineno + # note: `return start_lineno, end_lineno` doesn't seem to work. return start_lineno, start_lineno # XXX: dynamic context. return function name or something @@ -169,11 +170,8 @@ def _lines(self): # source_map should really be in CompilerData _, source_map = compile_ir.assembly_to_evm(c.assembly_runtime) - for _, v in source_map["pc_pos_map"].items(): - if v is None: - continue - (start_lineno, _, _, _) = v - ret.add(start_lineno) + for node in source_map["pc_raw_ast_map"].values(): + ret.add(node.lineno) return ret diff --git a/boa/interpret.py b/boa/interpret.py index 46c550f3..e4260da0 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -16,7 +16,7 @@ FilesystemInputBundle, ) from vyper.compiler.phases import CompilerData -from vyper.compiler.settings import anchor_settings, Settings +from vyper.compiler.settings import Settings, anchor_settings from vyper.semantics.types.module import ModuleT from vyper.utils import sha256sum diff --git a/boa/profiling.py b/boa/profiling.py index 6152f55d..982bfc27 100644 --- a/boa/profiling.py +++ b/boa/profiling.py @@ -135,13 +135,14 @@ def by_pc(self): @cached_property def by_line(self): ret = {} - line_map = self.contract.source_map["pc_pos_map"] + source_map = self.contract.source_map["pc_raw_ast_map"] current_line = None seen = set() for pc in self.computation.code._trace: - if line_map.get(pc) is not None: - current_line, _, _, _ = line_map[pc] + if (node := source_map.get(pc)) is not None: + current_line = node.lineno + # NOTE: do we still need the `current_line is not None` guard? if current_line is not None and pc not in seen: ret.setdefault(current_line, Datum()) ret[current_line].merge(self.by_pc[pc]) From 4ce1a7045d61784b79f04deac53c62717e010518 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 10 May 2024 11:21:43 +0200 Subject: [PATCH 24/48] expose private constants --- boa/contracts/vyper/vyper_contract.py | 23 +++++++++++++++++++++++ boa/interpret.py | 2 +- tests/unitary/test_get_constant.py | 15 +++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 tests/unitary/test_get_constant.py diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 8a804d79..83558031 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -28,6 +28,7 @@ from vyper.compiler.settings import OptimizationLevel, anchor_settings from vyper.exceptions import VyperException from vyper.ir.optimizer import optimize +from vyper.semantics.analysis.base import VarInfo from vyper.semantics.types import AddressT, HashMapT, TupleT from vyper.utils import method_id @@ -121,6 +122,10 @@ def at(self, address: Any) -> "VyperContract": return ret + @cached_property + def _constants(self): + return ConstantsModel(self.compiler_data) + # a few lines of shared code between VyperBlueprint and VyperContract class _BaseVyperContract(_BaseEVMContract): @@ -140,6 +145,10 @@ def __init__( def abi(self): return build_abi_output(self.compiler_data) + @cached_property + def _constants(self): + return ConstantsModel(self.compiler_data) + # create a blueprint for use with `create_from_blueprint`. # uses a ERC5202 preamble, when calling `create_from_blueprint` will @@ -454,6 +463,20 @@ def __repr__(self): return repr(self.dump()) +# data structure to represent the constants in a contract +class ConstantsModel: + def __init__(self, compiler_data: CompilerData): + for k, v in compiler_data.annotated_vyper_module._metadata["namespace"].items(): + if isinstance(v, VarInfo) and v.decl_node and v.is_constant: + setattr(self, k, v.decl_node.value.value) + + def dump(self): + return FrameDetail("constants", vars(self)) + + def __repr__(self): + return repr(self.dump()) + + class VyperContract(_BaseVyperContract): def __init__( self, diff --git a/boa/interpret.py b/boa/interpret.py index e4260da0..4049dd60 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -129,7 +129,7 @@ def compiler_data( global _disk_cache, _search_path file_input = FileInput( - source_code=source_code, + contents=source_code, source_id=-1, path=Path(contract_name), resolved_path=Path(filename), diff --git a/tests/unitary/test_get_constant.py b/tests/unitary/test_get_constant.py new file mode 100644 index 00000000..194af14f --- /dev/null +++ b/tests/unitary/test_get_constant.py @@ -0,0 +1,15 @@ +import boa + +code = """ +crvUSD: constant(address) = 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E +integer: constant(uint256) = 1518919871651 +""" + + +def test_get_constant(): + deployer = boa.loads_partial(code) + deployer._constants.crvUSD == "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + deployer._constants.crvUSD == 1518919871651 + contract = deployer.deploy() + contract._constants.crvUSD == "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + contract._constants.crvUSD == 1518919871651 From 4ea125f6209581218eb71e3ac1d2d416543a6a36 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 10 May 2024 15:48:56 +0200 Subject: [PATCH 25/48] fix test --- boa/contracts/vyper/vyper_contract.py | 8 ++++++-- tests/unitary/test_get_constant.py | 9 +++++---- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 83558031..8fbfb5d4 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -467,8 +467,12 @@ def __repr__(self): class ConstantsModel: def __init__(self, compiler_data: CompilerData): for k, v in compiler_data.annotated_vyper_module._metadata["namespace"].items(): - if isinstance(v, VarInfo) and v.decl_node and v.is_constant: - setattr(self, k, v.decl_node.value.value) + if ( + isinstance(v, VarInfo) + and v.decl_node is not None + and v.decl_node.is_constant + ): + setattr(self, k, v.decl_node.value.reduced().value) def dump(self): return FrameDetail("constants", vars(self)) diff --git a/tests/unitary/test_get_constant.py b/tests/unitary/test_get_constant.py index 194af14f..dc401f4c 100644 --- a/tests/unitary/test_get_constant.py +++ b/tests/unitary/test_get_constant.py @@ -3,13 +3,14 @@ code = """ crvUSD: constant(address) = 0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E integer: constant(uint256) = 1518919871651 +integer2: constant(uint256) = integer + 1 """ def test_get_constant(): deployer = boa.loads_partial(code) - deployer._constants.crvUSD == "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" - deployer._constants.crvUSD == 1518919871651 + assert deployer._constants.crvUSD == "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" + assert deployer._constants.integer == 1518919871651 + assert deployer._constants.integer2 == 1518919871652 contract = deployer.deploy() - contract._constants.crvUSD == "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" - contract._constants.crvUSD == 1518919871651 + assert contract._constants.crvUSD == "0xf939E0A03FB07F59A73314E73794Be0E57ac1b4E" From c7c06f378ab1341606c92515a8671ef2f7863071 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 10 May 2024 18:51:37 +0200 Subject: [PATCH 26/48] Fix some tests --- boa/contracts/vyper/vyper_contract.py | 4 ++-- boa/interpret.py | 2 +- tests/integration/fork/test_abi_contract.py | 13 +++++++------ tests/unitary/test_coverage.py | 2 +- tests/unitary/test_deploy_value.py | 2 +- 5 files changed, 12 insertions(+), 11 deletions(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 6918d4b4..46615132 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -25,7 +25,7 @@ from vyper.compiler import CompilerData from vyper.compiler import output as compiler_output from vyper.compiler.output import build_abi_output -from vyper.compiler.settings import OptimizationLevel, anchor_settings +from vyper.compiler.settings import OptimizationLevel, Settings, anchor_settings from vyper.exceptions import VyperException from vyper.ir.optimizer import optimize from vyper.semantics.types import AddressT, HashMapT, TupleT @@ -810,7 +810,7 @@ def unoptimized_bytecode(self): @cached_property def unoptimized_ir(self): - settings = self.compiler_data.settings.copy() + settings = Settings.from_dict(self.compiler_data.settings.as_dict()) settings.optimize = OptimizationLevel.NONE with anchor_settings(settings): return generate_ir_for_module(self.module_t) diff --git a/boa/interpret.py b/boa/interpret.py index e4260da0..4049dd60 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -129,7 +129,7 @@ def compiler_data( global _disk_cache, _search_path file_input = FileInput( - source_code=source_code, + contents=source_code, source_id=-1, path=Path(contract_name), resolved_path=Path(filename), diff --git a/tests/integration/fork/test_abi_contract.py b/tests/integration/fork/test_abi_contract.py index 12c6b8a0..dc6844ad 100644 --- a/tests/integration/fork/test_abi_contract.py +++ b/tests/integration/fork/test_abi_contract.py @@ -133,16 +133,17 @@ def test_fork_write(crvusd, n): def test_fork_write_flip(crvusd): e = boa.loads( f""" -from vyper.interfaces import ERC20 +from ethereum.ercs import ERC20 crvUSD: ERC20 -@external + +@deploy def __init__(): self.crvUSD = ERC20({crvusd.address}) @external def flip_from(_input: uint256) -> uint256: - self.crvUSD.transferFrom(msg.sender, self, _input) - self.crvUSD.transfer(msg.sender, _input / 2) - return _input / 2 + extcall self.crvUSD.transferFrom(msg.sender, self, _input) + extcall self.crvUSD.transfer(msg.sender, _input // 2) + return _input // 2 """ ) pool = "0x4dece678ceceb27446b35c672dc7d61f30bad69e" @@ -160,7 +161,7 @@ def test_abi_stack_trace(crvusd): from ethereum.ercs import ERC20 @external def foo(x: ERC20, from_: address): - x.transferFrom(from_, self, 100) + extcall x.transferFrom(from_, self, 100) """ ) diff --git a/tests/unitary/test_coverage.py b/tests/unitary/test_coverage.py index e7954b07..303a1797 100644 --- a/tests/unitary/test_coverage.py +++ b/tests/unitary/test_coverage.py @@ -29,7 +29,7 @@ def __init__(_foo_address: address): @external @view def bar(b: uint256) -> uint256: - c: uint256 = Foo(FOO).foo(b) + c: uint256 = staticcall Foo(FOO).foo(b) return c """ return boa.loads(source_code, external_contract.address, name="TestContract") diff --git a/tests/unitary/test_deploy_value.py b/tests/unitary/test_deploy_value.py index 34c03afc..43ad894a 100644 --- a/tests/unitary/test_deploy_value.py +++ b/tests/unitary/test_deploy_value.py @@ -3,7 +3,7 @@ import boa fund_me_source = """ -@external +@deploy @payable def __init__(): pass From 8e12cd7c3b8fa6098b29d63440c1595c75c3ac9f Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 13 May 2024 11:03:51 +0200 Subject: [PATCH 27/48] Fix more tests --- boa/contracts/vyper/ir_executor.py | 20 +++++++++++++------- boa/contracts/vyper/vyper_contract.py | 2 +- boa/profiling.py | 2 +- tests/integration/fork/test_logs.py | 8 ++++---- tests/unitary/test_gas_profiling.py | 2 +- tests/unitary/test_reverts.py | 19 ++++++++++--------- 6 files changed, 30 insertions(+), 23 deletions(-) diff --git a/boa/contracts/vyper/ir_executor.py b/boa/contracts/vyper/ir_executor.py index 46187e6a..0b07a3d4 100644 --- a/boa/contracts/vyper/ir_executor.py +++ b/boa/contracts/vyper/ir_executor.py @@ -11,6 +11,8 @@ from eth.exceptions import Revert as VMRevert from eth.exceptions import WriteProtection from eth_hash.auto import keccak +from vyper.ast.nodes import VyperNode +from vyper.codegen.ir_node import IRnode from vyper.compiler.phases import CompilerData from vyper.evm.opcodes import OPCODES from vyper.utils import unsigned_to_signed @@ -839,10 +841,11 @@ class Assert(IRExecutor): def _compile(self, test): _ = VMRevert # make flake8 happy + source = self.ir_node.ast_source self.builder.extend( f""" if not bool({test}): - VM.vyper_source_pos = {repr(self.ir_node.source_pos)} + VM.vyper_source_pos = {repr(source and source.src)} VM.vyper_error_msg = {repr(self.ir_node.error_msg)} raise VMRevert("") # venom assert """ @@ -855,10 +858,11 @@ class _IRRevert(IRExecutor): _sig = (int, int) def _compile(self, ptr, size): + source = self.ir_node.ast_source self.builder.extend( f""" VM.output = VM.memory_read_bytes({ptr}, {size}) - VM.vyper_source_pos = {repr(self.ir_node.source_pos)} + VM.vyper_source_pos = {repr(source and source.src)} VM.vyper_error_msg = {repr(self.ir_node.error_msg)} raise VMRevert(VM.output) # venom revert """ @@ -1118,17 +1122,19 @@ def compile(self, **kwargs): val.compile(out=variable.out_name, out_typ=int) -def _ensure_source_pos(ir_node, source_pos=None, error_msg=None): - if ir_node.source_pos is None: - ir_node.source_pos = source_pos +def _ensure_ast_source( + ir_node: IRnode, ast_source: VyperNode = None, error_msg: str = None +): + if ir_node.ast_source is None: + ir_node.ast_source = ast_source if ir_node.error_msg is None: ir_node.error_msg = error_msg for arg in ir_node.args: - _ensure_source_pos(arg, ir_node.source_pos, ir_node.error_msg) + _ensure_ast_source(arg, ir_node.ast_source, ir_node.error_msg) def executor_from_ir(ir_node, vyper_compiler_data) -> Any: - _ensure_source_pos(ir_node) + _ensure_ast_source(ir_node) ctx = CompileContext(vyper_compiler_data) ret = _executor_from_ir(ir_node, ctx) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 46615132..f6ce6895 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -633,7 +633,7 @@ def find_error_meta(self, computation): def find_source_of(self, computation, is_initcode=False): if hasattr(computation, "vyper_source_pos"): # this is set by ir executor currently. - return self.ast_map.get(computation.vyper_source_pos) + return self.source_map.get(computation.vyper_source_pos) code_stream = computation.code ast_map = self.source_map["pc_raw_ast_map"] diff --git a/boa/profiling.py b/boa/profiling.py index 982bfc27..e7466204 100644 --- a/boa/profiling.py +++ b/boa/profiling.py @@ -200,7 +200,7 @@ def get_line_data(self): line_gas_data = {} for (contract, line), datum in raw_summary: - fn_name = get_fn_name_from_lineno(contract.ast_map, line) + fn_name = get_fn_name_from_lineno(contract.source_map, line) # here we use net_gas to include child computation costs: line_info = LineInfo( diff --git a/tests/integration/fork/test_logs.py b/tests/integration/fork/test_logs.py index ac98be23..7e0bc58f 100644 --- a/tests/integration/fork/test_logs.py +++ b/tests/integration/fork/test_logs.py @@ -16,7 +16,7 @@ def deposit(): payable @external @payable def deposit(): - IWETH(weth9).deposit(value=msg.value) + extcall IWETH(weth9).deposit(value=msg.value) """ @@ -35,9 +35,9 @@ def test_logs(simple_contract): topic0 = keccak256("Deposit(address,uint256)".encode()) expected_log = ( 0, - int(WETH_ADDRESS, 16).to_bytes(20), - (int.from_bytes(topic0), int(simple_contract.address, 16)), - amount.to_bytes(32), + int(WETH_ADDRESS, 16).to_bytes(20, "big"), + (int.from_bytes(topic0, "big"), int(simple_contract.address, 16)), + amount.to_bytes(32, "big"), ) logs = simple_contract.get_logs() diff --git a/tests/unitary/test_gas_profiling.py b/tests/unitary/test_gas_profiling.py index 296b7189..3c351c0b 100644 --- a/tests/unitary/test_gas_profiling.py +++ b/tests/unitary/test_gas_profiling.py @@ -37,7 +37,7 @@ def __init__(_foo_address: address): @external @view def bar(b: uint256) -> uint256: - c: uint256 = Foo(FOO).foo(b) + c: uint256 = staticcall Foo(FOO).foo(b) return c """ return boa.loads(source_code, external_contract.address, name="TestContract") diff --git a/tests/unitary/test_reverts.py b/tests/unitary/test_reverts.py index 87fe3090..b23ac102 100644 --- a/tests/unitary/test_reverts.py +++ b/tests/unitary/test_reverts.py @@ -169,30 +169,31 @@ def test_reverts_dev_reason(): pool_code = """ @external @pure -def some_math(x: uint256): +def some_math(x: uint256) -> uint256: assert x < 10 # dev: math not ok + return x """ math_code = """ math: address interface Math: - def some_math(x: uint256): pure + def some_math(x: uint256) -> uint256: pure -@external +@deploy def __init__(math: address): self.math = math @external -def ext_call(): - Math(self.math).some_math(11) +def math_call(): + _: uint256 = staticcall Math(self.math).some_math(11) @external -def ext_call2(): - Math(self.math).some_math(11) # dev: call math +def math_call_with_reason(): + _: uint256 = staticcall Math(self.math).some_math(11) # dev: call math """ m = boa.loads(pool_code) p = boa.loads(math_code, m.address) with boa.reverts(dev="math not ok"): - p.ext_call() + p.math_call() with boa.reverts(dev="call math"): - p.ext_call2() + p.math_call_with_reason() From ae553ff8030bd6d9ac293757ea6704dde2c08092 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 13 May 2024 16:51:38 +0200 Subject: [PATCH 28/48] Use getpos to retrieve ast source --- boa/contracts/vyper/ir_executor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/boa/contracts/vyper/ir_executor.py b/boa/contracts/vyper/ir_executor.py index 0b07a3d4..9682109d 100644 --- a/boa/contracts/vyper/ir_executor.py +++ b/boa/contracts/vyper/ir_executor.py @@ -15,6 +15,7 @@ from vyper.codegen.ir_node import IRnode from vyper.compiler.phases import CompilerData from vyper.evm.opcodes import OPCODES +from vyper.ir.compile_ir import getpos from vyper.utils import unsigned_to_signed from boa.util.lrudict import lrudict @@ -845,7 +846,7 @@ def _compile(self, test): self.builder.extend( f""" if not bool({test}): - VM.vyper_source_pos = {repr(source and source.src)} + VM.vyper_source_pos = {repr(source and getpos(source))} VM.vyper_error_msg = {repr(self.ir_node.error_msg)} raise VMRevert("") # venom assert """ @@ -862,7 +863,7 @@ def _compile(self, ptr, size): self.builder.extend( f""" VM.output = VM.memory_read_bytes({ptr}, {size}) - VM.vyper_source_pos = {repr(source and source.src)} + VM.vyper_source_pos = {repr(source and getpos(source))} VM.vyper_error_msg = {repr(self.ir_node.error_msg)} raise VMRevert(VM.output) # venom revert """ From d6f0096412fe069f2ddfbe10a1c527715457347d Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Thu, 16 May 2024 20:28:34 +0200 Subject: [PATCH 29/48] Interface name --- tests/integration/RewardPool.vy | 8 ++++---- tests/integration/fork/test_abi_contract.py | 10 +++++----- tests/integration/veYFI.vy | 12 ++++++------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/integration/RewardPool.vy b/tests/integration/RewardPool.vy index 3a47521f..7000b2bb 100644 --- a/tests/integration/RewardPool.vy +++ b/tests/integration/RewardPool.vy @@ -3,7 +3,7 @@ @author Curve Finance, Yearn Finance @license MIT """ -from ethereum.ercs import ERC20 +from ethereum.ercs import IERC20 interface VotingYFI: def user_point_epoch(addr: address) -> uint256: view @@ -11,7 +11,7 @@ interface VotingYFI: def user_point_history(addr: address, loc: uint256) -> Point: view def point_history(loc: uint256) -> Point: view def checkpoint(): nonpayable - def token() -> ERC20: view + def token() -> IERC20: view def modify_lock(amount: uint256, unlock_time: uint256, user: address) -> LockedBalance: nonpayable event Initialized: @@ -47,7 +47,7 @@ struct LockedBalance: WEEK: constant(uint256) = 7 * 86400 TOKEN_CHECKPOINT_DEADLINE: constant(uint256) = 86400 -YFI: immutable(ERC20) +YFI: immutable(IERC20) VEYFI: immutable(VotingYFI) start_time: public(uint256) @@ -350,7 +350,7 @@ def toggle_allowed_to_relock(user: address) -> bool: @view @external -def token() -> ERC20: +def token() -> IERC20: return YFI diff --git a/tests/integration/fork/test_abi_contract.py b/tests/integration/fork/test_abi_contract.py index dc6844ad..89952752 100644 --- a/tests/integration/fork/test_abi_contract.py +++ b/tests/integration/fork/test_abi_contract.py @@ -133,12 +133,12 @@ def test_fork_write(crvusd, n): def test_fork_write_flip(crvusd): e = boa.loads( f""" -from ethereum.ercs import ERC20 -crvUSD: ERC20 +from ethereum.ercs import IERC20 +crvUSD: IERC20 @deploy def __init__(): - self.crvUSD = ERC20({crvusd.address}) + self.crvUSD = IERC20({crvusd.address}) @external def flip_from(_input: uint256) -> uint256: extcall self.crvUSD.transferFrom(msg.sender, self, _input) @@ -158,9 +158,9 @@ def flip_from(_input: uint256) -> uint256: def test_abi_stack_trace(crvusd): c = boa.loads( """ -from ethereum.ercs import ERC20 +from ethereum.ercs import IERC20 @external -def foo(x: ERC20, from_: address): +def foo(x: IERC20, from_: address): extcall x.transferFrom(from_, self, 100) """ ) diff --git a/tests/integration/veYFI.vy b/tests/integration/veYFI.vy index 46bcc2a7..8ab12801 100644 --- a/tests/integration/veYFI.vy +++ b/tests/integration/veYFI.vy @@ -10,7 +10,7 @@ Vote weight decays linearly over time. A user can unlock funds early incurring a penalty. """ -from ethereum.ercs import ERC20 +from ethereum.ercs import IERC20 interface RewardPool: def burn(amount: uint256) -> bool: nonpayable @@ -56,10 +56,10 @@ event Supply: ts: uint256 event Initialized: - token: ERC20 + token: IERC20 reward_pool: RewardPool -YFI: immutable(ERC20) +YFI: immutable(IERC20) REWARD_POOL: immutable(RewardPool) DAY: constant(uint256) = 86400 @@ -77,7 +77,7 @@ slope_changes: public(HashMap[address, HashMap[uint256, int128]]) # time -> sig @deploy -def __init__(token: ERC20, reward_pool: RewardPool): +def __init__(token: IERC20, reward_pool: RewardPool): """ @notice Contract constructor @param token YFI token address @@ -468,7 +468,7 @@ def getPriorVotes(user: address, height: uint256) -> uint256: def totalSupply(ts: uint256 = block.timestamp) -> uint256: """ @notice Calculate total voting power - @dev Adheres to the ERC20 `totalSupply` interface for Aragon compatibility + @dev Adheres to the IERC20 `totalSupply` interface for Aragon compatibility @return Total voting power """ epoch: uint256 = self.epoch[self] @@ -506,7 +506,7 @@ def totalSupplyAt(height: uint256) -> uint256: @view @external -def token() -> ERC20: +def token() -> IERC20: return YFI From 6fba877113eb3457cf21aa1ea4f44b1008a2cc35 Mon Sep 17 00:00:00 2001 From: Hubert Ritzdorf <10403309+ritzdorf@users.noreply.github.com> Date: Thu, 16 May 2024 20:37:26 +0200 Subject: [PATCH 30/48] Update FileInput to match vyper-0.4.0.rc5 --- boa/interpret.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boa/interpret.py b/boa/interpret.py index e4260da0..4049dd60 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -129,7 +129,7 @@ def compiler_data( global _disk_cache, _search_path file_input = FileInput( - source_code=source_code, + contents=source_code, source_id=-1, path=Path(contract_name), resolved_path=Path(filename), From 784c6b7d9d96f7d0639964b83b373e5eee07841f Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 14 May 2024 09:41:31 +0200 Subject: [PATCH 31/48] Add error when we cannot reach the backend Fixes #197 --- boa/integrations/jupyter/jupyter.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/boa/integrations/jupyter/jupyter.js b/boa/integrations/jupyter/jupyter.js index 310989a4..047ded94 100644 --- a/boa/integrations/jupyter/jupyter.js +++ b/boa/integrations/jupyter/jupyter.js @@ -87,10 +87,22 @@ /** Call the backend when the given function is called, handling errors */ const handleCallback = func => async (token, ...args) => { if (!colab) { - // Check if the cell was already executed. In Colab, eval_js() doesn't replay. + // Check backend and whether cell was executed. In Colab, eval_js() doesn't replay. const response = await fetch(`${base}/titanoboa_jupyterlab/callback/${token}`); - // !response.ok indicates the cell has already been executed - if (!response.ok) return; + if (response.status === 404 && response.headers.get('Content-Type') === 'application/json') { + return; // the cell has already been executed + } + if (!response.ok) { + const error = 'Could not connect to the titanoboa backend. Please make sure the Jupyter extension is installed by running the following command:'; + const command = 'jupyter lab extension enable boa'; + if (element) { + element.style.display = "block"; // show the output element in JupyterLab + element.innerHTML = `

${error}

${command}
`; + } else { + prompt(error, command); + } + return; + } } const body = stringify(await parsePromise(func(...args))); From 4d3478b770461f45e9a19f15e48e5b3ed7737da0 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 14 May 2024 09:50:31 +0200 Subject: [PATCH 32/48] Update documentation --- README.md | 3 +- docs/source/testing.rst | 3 +- examples/jupyter_browser_signer.ipynb | 132 ++++---------------------- 3 files changed, 25 insertions(+), 113 deletions(-) diff --git a/README.md b/README.md index fb311d6e..a5e9b97b 100644 --- a/README.md +++ b/README.md @@ -209,7 +209,8 @@ Cast current deployed addresses to vyper contract ### Jupyter Integration -You can use Jupyter to execute titanoboa code in network mode from your browser using any wallet, using `boa.integrations.jupyter.BrowserSigner` as a drop-in replacement for `eth_account.Account`. For a full example, please see [this example Jupyter notebook](examples/jupyter_browser_signer.ipynb) +You can use Jupyter to execute titanoboa code in network mode from your browser using any wallet, using `BrowserSigner` as a drop-in replacement for `eth_account.Account`. +For a full example, please see [this example Jupyter notebook](examples/jupyter_browser_signer.ipynb) ### Basic tests diff --git a/docs/source/testing.rst b/docs/source/testing.rst index 71075f27..cbfc90e1 100644 --- a/docs/source/testing.rst +++ b/docs/source/testing.rst @@ -151,7 +151,8 @@ ipython Vyper Cells Titanoboa supports ipython Vyper cells. This means that you can write Vyper code in a ipython/Jupyter Notebook environment and execute it as if it was a Python cell (the contract will be compiled instead, and a ``ContractFactory`` will be returned). -To enable this feature, execute ``%load_ext boa.ipython`` in a cell. +You can use Jupyter to execute titanoboa code in network mode from your browser using any wallet, using your wallet to sign transactions and call the RPC. +For a full example, please see `this example Jupyter notebook <../../examples/jupyter_browser_signer.ipynb>`_. .. code-block:: python diff --git a/examples/jupyter_browser_signer.ipynb b/examples/jupyter_browser_signer.ipynb index 886eb00b..e50fcfd9 100644 --- a/examples/jupyter_browser_signer.ipynb +++ b/examples/jupyter_browser_signer.ipynb @@ -1,111 +1,33 @@ { "cells": [ { - "cell_type": "code", - "execution_count": 1, - "id": "94744db8", "metadata": {}, - "outputs": [], + "cell_type": "markdown", "source": [ - "import boa; from boa.network import NetworkEnv" - ] + "Before being able to use the plugin, you need to install it. You can do this by running the following command:\n", + "```bash\n", + "jupyter lab extension enable boa\n", + "```\n", + "\n", + "We provide a multi-user setup with JupyterLab in [try.vyperlang.org](https://try.vyperlang.org/).\n", + "The source code for this website is available in [GitHub](https://github.com/vyperlang/try.vyperlang.org).\n", + "\n", + "It is also possible to run our plugin in [Google Colab](https://colab.research.google.com/).\n", + "To do this, you need to install the plugin by running the following commands:\n", + "```jupyter\n", + "!pip install git+https://github.com/vyperlang/titanoboa\n", + "%load_ext boa.ipython\n", + "```" + ], + "id": "1eb33cb4f02a9a6c" }, { "cell_type": "code", - "execution_count": 2, - "id": "ff9dfb06", + "execution_count": 1, + "id": "94744db8", "metadata": {}, "outputs": [], - "source": [ - "%load_ext boa.ipython" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "9f241bf5", - "metadata": {}, - "outputs": [ - { - "data": { - "application/javascript": [ - "\n", - "require.config({\n", - " paths: {\n", - " //ethers: \"https://cdnjs.cloudflare.com/ajax/libs/ethers/5.7.2/ethers.umd.min\"\n", - " ethers: \"https://cdnjs.cloudflare.com/ajax/libs/ethers/6.4.2/ethers.umd.min\"\n", - " }\n", - "});\n", - "\n", - "require(['ethers'], function(ethers) {\n", - " // Initialize ethers\n", - " let provider = new ethers.BrowserProvider(window.ethereum);\n", - "\n", - " // check that we have a signer for this account\n", - " Jupyter.notebook.kernel.comm_manager.register_target('get_signer', function(c, msg) {\n", - " // console.log(\"get_signer created\", c)\n", - " c.on_msg(function(msg) {\n", - " // console.log(\"get_signer called\", c)\n", - " let account = msg.content.data.account\n", - " provider.getSigner(account).then(signer => {\n", - " // console.log(\"success\", signer)\n", - " c.send({\"success\": signer});\n", - " }).catch(function(error) {\n", - " console.error(\"got error, percolating up:\", error);\n", - " c.send({\"error\": error});\n", - " });\n", - " });\n", - " });\n", - "\n", - " Jupyter.notebook.kernel.comm_manager.register_target(\"send_transaction\", function(c, msg) {\n", - " c.on_msg(function(msg) {\n", - " let tx_data = msg.content.data.transaction_data;\n", - " let account = msg.content.data.account\n", - " provider.getSigner(account).then(signer => {\n", - " signer.sendTransaction(tx_data).then(response => {\n", - " console.log(response);\n", - " c.send({\"success\": response});\n", - " }).catch(function(error) {\n", - " console.error(\"got error, percolating up:\", error);\n", - " c.send({\"error\": error});\n", - " });\n", - " }).catch(function(error) {\n", - " console.error(\"got error, percolating up:\", error);\n", - " c.send({\"error\": error});\n", - " });\n", - " });\n", - " });\n", - "});\n", - "\n", - "Jupyter.notebook.kernel.comm_manager.register_target(\"test_comm\", function(comm, msg) {\n", - " console.log(\"ENTER\", comm);\n", - " /*comm.on_close(function(msg) {\n", - " console.log(\"CLOSING\", msg);\n", - " });\n", - " */\n", - "\n", - " comm.on_msg(function(msg) {\n", - " console.log(\"ENTER 2\", comm);\n", - " console.log(\"ENTER 3\", msg.content.data);\n", - " setTimeout(() => {\n", - " comm.send({\"success\": \"hello\", \"echo\": msg.content.data});\n", - " comm.close();\n", - " console.log(comm);\n", - " }, 350);\n", - " });\n", - "});\n" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "from boa.integrations.jupyter import BrowserSigner" - ] + "source": "import boa" }, { "cell_type": "code", @@ -113,19 +35,7 @@ "id": "814ff4f3", "metadata": {}, "outputs": [], - "source": [ - "boa.set_env(NetworkEnv(\"\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "a24872c9", - "metadata": {}, - "outputs": [], - "source": [ - "boa.env.add_account(BrowserSigner())" - ] + "source": "boa.set_browser_env() # this will use the browser signer and the browser RPC" }, { "cell_type": "code", From b163e169564ffbd0c6ad76c78b8d56d77b585a76 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 14 May 2024 14:35:49 +0200 Subject: [PATCH 33/48] Move Jupyter installation to README --- README.md | 40 +++++++++++++++++++++++- examples/jupyter_browser_signer.ipynb | 44 ++++++++------------------- 2 files changed, 52 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index a5e9b97b..b7e39576 100644 --- a/README.md +++ b/README.md @@ -209,9 +209,47 @@ Cast current deployed addresses to vyper contract ### Jupyter Integration -You can use Jupyter to execute titanoboa code in network mode from your browser using any wallet, using `BrowserSigner` as a drop-in replacement for `eth_account.Account`. +You can use Jupyter to execute titanoboa code in network mode from your browser using any wallet. +We provide a `BrowserSigner` as a drop-in replacement for `eth_account.Account`. +The `BrowserRPC` may be used to interact with the RPC server from the browser. + For a full example, please see [this example Jupyter notebook](examples/jupyter_browser_signer.ipynb) +#### JupyterLab + +Before being able to use the plugin, you need to install it. +You can do this by running the following command in the terminal: + +```bash +pip install titanoboa +jupyter lab extension enable boa +``` +To activate our IPython extension, you need to run the following command in the notebook: +```jupyter +%load_ext boa.ipython +``` + +For ease of use, add the following to `ipython_config.py`: +```python +c.InteractiveShellApp.extensions = ["boa.ipython"] +c.InteractiveShellApp.exec_lines = ['import boa'] +``` + +We provide a multi-user setup with JupyterLab in [try.vyperlang.org](https://try.vyperlang.org/), where the extension is installed and activated. +The source code for this website is available in the [GitHub repository](https://github.com/vyperlang/try.vyperlang.org). + +#### Colab +It is also possible to run our plugin in [Google Colab](https://colab.research.google.com/). +To do this, you need to install the plugin by running the following commands: +```jupyter +!pip install titanoboa +%load_ext boa.ipython +``` + +#### IPython extensions + +This activates the `%%vyper`, `%%contract` and `%%eval` magics. + ### Basic tests diff --git a/examples/jupyter_browser_signer.ipynb b/examples/jupyter_browser_signer.ipynb index e50fcfd9..5da5f571 100644 --- a/examples/jupyter_browser_signer.ipynb +++ b/examples/jupyter_browser_signer.ipynb @@ -1,45 +1,27 @@ { "cells": [ - { - "metadata": {}, - "cell_type": "markdown", - "source": [ - "Before being able to use the plugin, you need to install it. You can do this by running the following command:\n", - "```bash\n", - "jupyter lab extension enable boa\n", - "```\n", - "\n", - "We provide a multi-user setup with JupyterLab in [try.vyperlang.org](https://try.vyperlang.org/).\n", - "The source code for this website is available in [GitHub](https://github.com/vyperlang/try.vyperlang.org).\n", - "\n", - "It is also possible to run our plugin in [Google Colab](https://colab.research.google.com/).\n", - "To do this, you need to install the plugin by running the following commands:\n", - "```jupyter\n", - "!pip install git+https://github.com/vyperlang/titanoboa\n", - "%load_ext boa.ipython\n", - "```" - ], - "id": "1eb33cb4f02a9a6c" - }, { "cell_type": "code", "execution_count": 1, "id": "94744db8", "metadata": {}, "outputs": [], - "source": "import boa" + "source": "%load_ext boa.ipython" }, { - "cell_type": "code", - "execution_count": 4, - "id": "814ff4f3", "metadata": {}, + "cell_type": "code", "outputs": [], - "source": "boa.set_browser_env() # this will use the browser signer and the browser RPC" + "execution_count": 2, + "source": [ + "import boa\n", + "boa.set_browser_env() # this will use the browser signer and the browser RPC" + ], + "id": "b724995f3df612f0" }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 3, "id": "1e98969d", "metadata": {}, "outputs": [ @@ -49,7 +31,7 @@ "" ] }, - "execution_count": 6, + "execution_count": 3, "metadata": {}, "output_type": "execute_result" } @@ -68,7 +50,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 4, "id": "c5b60ed3", "metadata": {}, "outputs": [ @@ -88,7 +70,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 5, "id": "bdbfc09c", "metadata": {}, "outputs": [ @@ -99,7 +81,7 @@ "': 1000}>" ] }, - "execution_count": 8, + "execution_count": 5, "metadata": {}, "output_type": "execute_result" } From 3e61164386bd8f2edd1ff92fa85f0b6743cb37a4 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 17 May 2024 15:51:19 +0200 Subject: [PATCH 34/48] refactor: extract _get_ir_pos function --- boa/contracts/vyper/ir_executor.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/boa/contracts/vyper/ir_executor.py b/boa/contracts/vyper/ir_executor.py index 9682109d..dbb1fffd 100644 --- a/boa/contracts/vyper/ir_executor.py +++ b/boa/contracts/vyper/ir_executor.py @@ -842,11 +842,10 @@ class Assert(IRExecutor): def _compile(self, test): _ = VMRevert # make flake8 happy - source = self.ir_node.ast_source self.builder.extend( f""" if not bool({test}): - VM.vyper_source_pos = {repr(source and getpos(source))} + VM.vyper_source_pos = {repr(_get_ir_pos(self.ir_node))} VM.vyper_error_msg = {repr(self.ir_node.error_msg)} raise VMRevert("") # venom assert """ @@ -859,11 +858,10 @@ class _IRRevert(IRExecutor): _sig = (int, int) def _compile(self, ptr, size): - source = self.ir_node.ast_source self.builder.extend( f""" VM.output = VM.memory_read_bytes({ptr}, {size}) - VM.vyper_source_pos = {repr(source and getpos(source))} + VM.vyper_source_pos = {repr(_get_ir_pos(self.ir_node))} VM.vyper_error_msg = {repr(self.ir_node.error_msg)} raise VMRevert(VM.output) # venom revert """ @@ -1164,3 +1162,9 @@ def _executor_from_ir(ir_node, compile_ctx) -> Any: assert len(ir_node.args) == 0, ir_node assert isinstance(ir_node.value, str) return StringExecutor(compile_ctx, ir_node.value) + + +def _get_ir_pos(ir_node): + if ir_node.ast_source is None: + return None + return getpos(ir_node.ast_source) From b431d4e7eb7b6e8ef286e90bf9728af9d0544707 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 17 May 2024 15:53:56 +0200 Subject: [PATCH 35/48] Use copy.copy --- boa/contracts/vyper/vyper_contract.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index f6ce6895..26be8a8a 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -25,7 +25,7 @@ from vyper.compiler import CompilerData from vyper.compiler import output as compiler_output from vyper.compiler.output import build_abi_output -from vyper.compiler.settings import OptimizationLevel, Settings, anchor_settings +from vyper.compiler.settings import OptimizationLevel, anchor_settings from vyper.exceptions import VyperException from vyper.ir.optimizer import optimize from vyper.semantics.types import AddressT, HashMapT, TupleT @@ -810,7 +810,7 @@ def unoptimized_bytecode(self): @cached_property def unoptimized_ir(self): - settings = Settings.from_dict(self.compiler_data.settings.as_dict()) + settings = copy.copy(self.compiler_data.settings) settings.optimize = OptimizationLevel.NONE with anchor_settings(settings): return generate_ir_for_module(self.module_t) From 1d24c2b66bd86048ecc853dee9f80efc4949c9fd Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 17 May 2024 16:34:14 +0200 Subject: [PATCH 36/48] Review comment --- boa/contracts/vyper/vyper_contract.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index bacdd002..ed4b7773 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -14,6 +14,7 @@ import vyper.ir.compile_ir as compile_ir import vyper.semantics.namespace as vy_ns from eth.exceptions import VMError +from vyper.ast.nodes import VariableDecl from vyper.ast.parse import parse_to_ast from vyper.codegen.core import calculate_type_for_external_return from vyper.codegen.function_definitions import ( @@ -28,7 +29,6 @@ from vyper.compiler.settings import OptimizationLevel, anchor_settings from vyper.exceptions import VyperException from vyper.ir.optimizer import optimize -from vyper.semantics.analysis.base import VarInfo from vyper.semantics.types import AddressT, HashMapT, TupleT from vyper.utils import method_id @@ -474,13 +474,9 @@ def __repr__(self): # data structure to represent the constants in a contract class ConstantsModel: def __init__(self, compiler_data: CompilerData): - for k, v in compiler_data.annotated_vyper_module._metadata["namespace"].items(): - if ( - isinstance(v, VarInfo) - and v.decl_node is not None - and v.decl_node.is_constant - ): - setattr(self, k, v.decl_node.value.reduced().value) + for v in compiler_data.annotated_vyper_module.get_children(VariableDecl): + if v.is_constant: + setattr(self, v.target.id, v.value.reduced().value) def dump(self): return FrameDetail("constants", vars(self)) From e655529bb5c96c075ca26b01fb420b1914e82b73 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Tue, 21 May 2024 12:54:09 +0200 Subject: [PATCH 37/48] handle stack traces in constructor --- boa/contracts/vyper/vyper_contract.py | 9 ++++++--- tests/unitary/test_reverts.py | 28 ++++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 4 deletions(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 80ae2b3e..92deedb5 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -494,6 +494,7 @@ def __init__( compiler_data.global_ctx.init_function.decl_node, self ) + self._address = None if skip_initcode: if value: raise Exception("nonzero value but initcode is being skipped") @@ -619,9 +620,11 @@ def module_t(self): def source_map(self): if self._source_map is None: with anchor_settings(self.compiler_data.settings): - _, self._source_map = compile_ir.assembly_to_evm( - self.compiler_data.assembly_runtime - ) + if self._address is None: + assembly = self.compiler_data.assembly + else: + assembly = self.compiler_data.assembly_runtime + _, self._source_map = compile_ir.assembly_to_evm(assembly) return self._source_map def find_error_meta(self, computation): diff --git a/tests/unitary/test_reverts.py b/tests/unitary/test_reverts.py index 411150ec..ea047cfa 100644 --- a/tests/unitary/test_reverts.py +++ b/tests/unitary/test_reverts.py @@ -202,7 +202,7 @@ def math_call_with_reason(): def test_trace_constructor_revert(): code = """ -@external +@deploy def __init__(): assert False, "revert reason" """ @@ -210,3 +210,29 @@ def __init__(): boa.loads(code) assert "revert reason" in str(error_context.value) + + +def test_trace_constructor_stack_trace(): + called_code = """ +@external +@pure +def check(x: uint256) -> uint256: + assert x < 10 # dev: less than 10 + return x +""" + caller_code = """ +interface Called: + def check(x: uint256) -> uint256: pure + +@deploy +def __init__(math: address, x: uint256): + _: uint256 = staticcall Called(math).check(x) +""" + called = boa.loads(called_code) + boa.loads(caller_code, called.address, 0) + with pytest.raises(BoaError) as error_context: + boa.loads(caller_code, called.address, 10) + + trace = error_context.value.stack_trace + assert [repr(frame.vm_error) for frame in trace] == ["Revert(b'')"] * 2 + assert [frame.dev_reason.reason_str for frame in trace] == ["less than 10"] * 2 From 594302dfc283812e497b32ee0b3599ff45f4e94d Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 24 May 2024 16:39:11 +0200 Subject: [PATCH 38/48] get_folded_value + comment --- boa/contracts/vyper/vyper_contract.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index ed4b7773..50aca3aa 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -124,6 +124,7 @@ def at(self, address: Any) -> "VyperContract": @cached_property def _constants(self): + # Make constants available at compile time. Useful for testing. See #196 return ConstantsModel(self.compiler_data) @@ -476,7 +477,7 @@ class ConstantsModel: def __init__(self, compiler_data: CompilerData): for v in compiler_data.annotated_vyper_module.get_children(VariableDecl): if v.is_constant: - setattr(self, v.target.id, v.value.reduced().value) + setattr(self, v.target.id, v.value.get_folded_value().value) def dump(self): return FrameDetail("constants", vars(self)) From 94d226119a3a3693e82729b07bf7d950b6407c08 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 24 May 2024 16:42:52 +0200 Subject: [PATCH 39/48] Lint --- boa/interpret.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boa/interpret.py b/boa/interpret.py index 969590da..242a4bac 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -124,7 +124,7 @@ def get_module_fingerprint( def compiler_data( - source_code: str, contract_name: str, filename: str, **kwargs + source_code: str, contract_name: str, filename: str | Path, **kwargs ) -> CompilerData: global _disk_cache, _search_path From de8fc4b066b60055a5743de8ab7b802277894635 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 27 May 2024 11:13:34 +0200 Subject: [PATCH 40/48] Update test to 0.4.0 --- tests/unitary/test_logs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unitary/test_logs.py b/tests/unitary/test_logs.py index eab2bfcd..e43bbddd 100644 --- a/tests/unitary/test_logs.py +++ b/tests/unitary/test_logs.py @@ -9,7 +9,7 @@ def test_log_constructor(): receiver: indexed(address) value: uint256 -@external +@deploy def __init__(supply: uint256): log Transfer(empty(address), msg.sender, supply) """, From 0e0f9cd1204512e4f82329554bbe5c7c36bfe00d Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 27 May 2024 11:30:08 +0200 Subject: [PATCH 41/48] Fix events for 0.4.0 --- boa/contracts/vyper/vyper_contract.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 5063ca82..355caa46 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -711,8 +711,10 @@ def get_logs(self, computation=None, include_child_logs=True): @cached_property def event_for(self): - m = self.compiler_data.vyper_module_folded._metadata["type"] - return {e.event_id: e for e in m.events.values()} + event_types = [ + d._metadata["event_type"] for d in self.compiler_data.global_ctx.event_defs + ] + return {e.event_id: e for e in event_types} def decode_log(self, e): log_id, address, topics, data = e From a29d9562efc4bf97a21e3ac74169ebf2154afbd9 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 29 May 2024 16:24:19 +0200 Subject: [PATCH 42/48] Review comments --- boa/contracts/vyper/vyper_contract.py | 38 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 355caa46..a5122714 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -558,15 +558,24 @@ def _run_init(self, *args, value=0, override_address=None): encoded_args = self._ctor.prepare_calldata(*args) initcode = self.compiler_data.bytecode + encoded_args - address, computation = self.env.deploy( - bytecode=initcode, value=value, override_address=override_address - ) - self._computation = computation - self.bytecode = computation.output + with self._anchor_source_map(self._deployment_source_map): + address, computation = self.env.deploy( + bytecode=initcode, value=value, override_address=override_address + ) - if computation.is_error: - raise BoaError(self.stack_trace(computation)) - return address + self._computation = computation + self.bytecode = computation.output + + if computation.is_error: + raise BoaError(self.stack_trace(computation)) + + return address + + @cached_property + def _deployment_source_map(self): + with anchor_settings(self.compiler_data.settings): + _, source_map = compile_ir.assembly_to_evm(self.compiler_data.assembly) + return source_map # manually set the runtime bytecode, instead of using deploy def _set_bytecode(self, bytecode: bytes) -> None: @@ -647,10 +656,7 @@ def module_t(self): def source_map(self): if self._source_map is None: with anchor_settings(self.compiler_data.settings): - if self._address is None: - assembly = self.compiler_data.assembly - else: - assembly = self.compiler_data.assembly_runtime + assembly = self.compiler_data.assembly_runtime _, self._source_map = compile_ir.assembly_to_evm(assembly) return self._source_map @@ -711,10 +717,8 @@ def get_logs(self, computation=None, include_child_logs=True): @cached_property def event_for(self): - event_types = [ - d._metadata["event_type"] for d in self.compiler_data.global_ctx.event_defs - ] - return {e.event_id: e for e in event_types} + module_t = self.compiler_data.global_ctx + return {e.event_id: e for e in module_t.used_events} def decode_log(self, e): log_id, address, topics, data = e @@ -1031,6 +1035,8 @@ def __call__(self, *args, value=0, gas=None, sender=None, **kwargs): if hasattr(self, "_override_bytecode"): override_bytecode = self._override_bytecode + # note: this anchor doesn't do anything on the default implementation. + # it's override the source map in subclasses with self.contract._anchor_source_map(self._source_map): computation = self.env.execute_code( to_address=self.contract._address, From 2a3e9e8600cca8d9fe6c1168247b563511eca162 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Wed, 29 May 2024 16:29:46 +0200 Subject: [PATCH 43/48] unneeded line, it's already in base class --- boa/contracts/vyper/vyper_contract.py | 1 - 1 file changed, 1 deletion(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index a5122714..768ced9a 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -522,7 +522,6 @@ def __init__( compiler_data.global_ctx.init_function.decl_node, self ) - self._address = None if skip_initcode: if value: raise Exception("nonzero value but initcode is being skipped") From 33305762264cb2cac4751f7f6b4c773f660e0226 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Fri, 31 May 2024 09:19:02 +0200 Subject: [PATCH 44/48] Fix comment typo Co-authored-by: Charles Cooper --- boa/contracts/vyper/vyper_contract.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/boa/contracts/vyper/vyper_contract.py b/boa/contracts/vyper/vyper_contract.py index 768ced9a..a716635e 100644 --- a/boa/contracts/vyper/vyper_contract.py +++ b/boa/contracts/vyper/vyper_contract.py @@ -1035,7 +1035,7 @@ def __call__(self, *args, value=0, gas=None, sender=None, **kwargs): override_bytecode = self._override_bytecode # note: this anchor doesn't do anything on the default implementation. - # it's override the source map in subclasses + # the source map is overridden in subclasses with self.contract._anchor_source_map(self._source_map): computation = self.env.execute_code( to_address=self.contract._address, From 67f33f5be8779e10bb8fcc3791a483f6e2816401 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 17 Jun 2024 14:00:16 +0200 Subject: [PATCH 45/48] Fix tests --- boa/interpret.py | 3 ++- .../contracts/vyper/test_vyper_contract.py | 10 +++---- tests/unitary/test_call_internal_fn.py | 2 +- tests/unitary/test_reverts.py | 2 +- tests/unitary/utils/test_cache.py | 26 ++++++++++++++----- 5 files changed, 28 insertions(+), 15 deletions(-) diff --git a/boa/interpret.py b/boa/interpret.py index 1d9f5dd7..a753c9ea 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -152,6 +152,7 @@ def compiler_data( fingerprint = get_module_fingerprint(module_t) def get_compiler_data(): + print(f"miss: {cache_key}") with anchor_settings(ret.settings): # force compilation to happen so DiskCache will cache the compiled artifact: _ = ret.bytecode, ret.bytecode_runtime @@ -214,7 +215,7 @@ def loads_partial( compiler_args = compiler_args or {} deployer_class = _get_default_deployer_class() - data = compiler_data(source_code, name, deployer_class, **compiler_args) + data = compiler_data(source_code, name, filename, deployer_class, **compiler_args) return deployer_class(data, filename=filename) diff --git a/tests/unitary/contracts/vyper/test_vyper_contract.py b/tests/unitary/contracts/vyper/test_vyper_contract.py index 6627187c..6f67b154 100644 --- a/tests/unitary/contracts/vyper/test_vyper_contract.py +++ b/tests/unitary/contracts/vyper/test_vyper_contract.py @@ -9,7 +9,7 @@ def test_decode_struct(): point: Point -@external +@deploy def __init__(): self.point = Point({x: 1, y: 2}) """ @@ -21,7 +21,7 @@ def test_decode_tuple(): code = """ point: (int8, int8) -@external +@deploy def __init__(): self.point[0] = 1 self.point[1] = 2 @@ -33,7 +33,7 @@ def test_decode_string_array(): code = """ point: int8[2] -@external +@deploy def __init__(): self.point[0] = 1 self.point[1] = 2 @@ -45,7 +45,7 @@ def test_decode_bytes_m(): code = """ b: bytes2 -@external +@deploy def __init__(): self.b = 0xd9b6 """ @@ -56,7 +56,7 @@ def test_decode_dynarray(): code = """ point: DynArray[int8, 10] -@external +@deploy def __init__(): self.point = [1, 2] """ diff --git a/tests/unitary/test_call_internal_fn.py b/tests/unitary/test_call_internal_fn.py index 374209e4..f04ac3d0 100644 --- a/tests/unitary/test_call_internal_fn.py +++ b/tests/unitary/test_call_internal_fn.py @@ -37,7 +37,7 @@ def _test_bool(a: uint256, b: bool = False) -> bool: @internal def _test_repeat(z: int128) -> int128: x: int128 = 0 - for i in range(6): + for i: int128 in range(6): x = x + z return x diff --git a/tests/unitary/test_reverts.py b/tests/unitary/test_reverts.py index 341acbf4..11fd90b1 100644 --- a/tests/unitary/test_reverts.py +++ b/tests/unitary/test_reverts.py @@ -216,7 +216,7 @@ def foo(x: uint256): nonpayable @external def revert(contract: HasFoo): - contract.foo(5) + extcall contract.foo(5) """ ) diff --git a/tests/unitary/utils/test_cache.py b/tests/unitary/utils/test_cache.py index dc16e64c..27a731f3 100644 --- a/tests/unitary/utils/test_cache.py +++ b/tests/unitary/utils/test_cache.py @@ -1,3 +1,6 @@ +from vyper.compiler import CompilerData + +from boa.contracts.vyper.vyper_contract import VyperDeployer from boa.interpret import _disk_cache, compiler_data @@ -6,10 +9,19 @@ def test_cache_contract_name(): x: constant(int128) = 1000 """ assert _disk_cache is not None - test1 = compiler_data(code, "test1") - test2 = compiler_data(code, "test2") - assert ( - test1.__dict__ == compiler_data(code, "test1").__dict__ - ), "Should hit the cache" - assert test1.__dict__ != test2.__dict__, "Should be different objects" - assert test2.contract_name == "test2" + test1 = compiler_data(code, "test1", __file__, VyperDeployer) + test2 = compiler_data(code, "test2", __file__, VyperDeployer) + test3 = compiler_data(code, "test1", __file__, VyperDeployer) + assert _to_dict(test1) == _to_dict(test3), "Should hit the cache" + assert _to_dict(test1) != _to_dict(test2), "Should be different objects" + assert str(test2.contract_path) == "test2" + + +def _to_dict(data: CompilerData) -> dict: + """ + Serialize the `CompilerData` object to a dictionary for comparison. + """ + d = data.__dict__.copy() + d["input_bundle"] = d["input_bundle"].__dict__.copy() + d["input_bundle"]["_cache"] = d["input_bundle"]["_cache"].__dict__.copy() + return d From e2fcc69e1497807be4ba2aee736140f3e58fd074 Mon Sep 17 00:00:00 2001 From: Daniel Schiavini Date: Mon, 17 Jun 2024 14:16:07 +0200 Subject: [PATCH 46/48] Bad print --- boa/interpret.py | 1 - 1 file changed, 1 deletion(-) diff --git a/boa/interpret.py b/boa/interpret.py index a753c9ea..c9745bac 100644 --- a/boa/interpret.py +++ b/boa/interpret.py @@ -152,7 +152,6 @@ def compiler_data( fingerprint = get_module_fingerprint(module_t) def get_compiler_data(): - print(f"miss: {cache_key}") with anchor_settings(ret.settings): # force compilation to happen so DiskCache will cache the compiled artifact: _ = ret.bytecode, ret.bytecode_runtime From e7efc7f3ef73b7f552a806b30dfe15ed638eb994 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 18 Jul 2024 19:02:24 -0700 Subject: [PATCH 47/48] fix vyper pin now that it's been released --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 0f017d45..1649b108 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,7 +15,7 @@ classifiers = ["Topic :: Software Development"] # Requirements dependencies = [ - "vyper @ git+https://github.com/vyperlang/vyper.git@master", + "vyper>=0.4.0", "eth-stdlib>=0.2.7,<0.3.0", "eth-abi", "py-evm>=0.10.0b4", From 44956ed4756d2b600c26ddc4767741caf441f1bb Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Tue, 23 Jul 2024 18:27:17 +0800 Subject: [PATCH 48/48] bump version --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dc389f97..eaf73aad 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "titanoboa" -version = "0.1.10b1" +version = "0.1.10" description = "A Vyper interpreter" #authors = [] license = { file = "LICENSE" }