diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8d491e2530..aa2155db20 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -116,6 +116,7 @@ jobs: # modes across all python versions - one is enough - python-version: ["3.10", "310"] - python-version: ["3.12", "312"] + - python-version: ["3.13", "313"] # os-specific rules - os: windows diff --git a/docs/using-modules.rst b/docs/using-modules.rst index 7d63eb6617..4400a8dfa8 100644 --- a/docs/using-modules.rst +++ b/docs/using-modules.rst @@ -62,6 +62,21 @@ The ``_times_two()`` helper function in the above module can be immediately used The other functions cannot be used yet, because they touch the ``ownable`` module's state. There are two ways to declare a module so that its state can be used. +Using a module as an interface +============================== + +A module can be used as an interface with the ``__at__`` syntax. + +.. code-block:: vyper + + import ownable + + an_ownable: ownable.__interface__ + + def call_ownable(addr: address): + self.an_ownable = ownable.__at__(addr) + self.an_ownable.transfer_ownership(...) + Initializing a module ===================== diff --git a/setup.py b/setup.py index 5d6bd1db3a..69a08b737c 100644 --- a/setup.py +++ b/setup.py @@ -113,6 +113,7 @@ def _global_version(version): "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ], package_data={"vyper.ast": ["grammar.lark"]}, data_files=[("", [hash_file_rel_path])], diff --git a/tests/functional/codegen/features/test_logging.py b/tests/functional/codegen/features/test_logging.py index 2bb646e6ef..87d848fae5 100644 --- a/tests/functional/codegen/features/test_logging.py +++ b/tests/functional/codegen/features/test_logging.py @@ -1254,6 +1254,23 @@ def foo(): assert log.topics == [event_id, topic1, topic2, topic3] +valid_list = [ + # test constant folding inside raw_log + """ +topic: constant(bytes32) = 0x1212121212121210212801291212121212121210121212121212121212121212 + +@external +def foo(): + raw_log([[topic]][0], b'') + """ +] + + +@pytest.mark.parametrize("code", valid_list) +def test_raw_log_pass(code): + assert compile_code(code) is not None + + fail_list = [ ( """ diff --git a/tests/functional/codegen/modules/test_exports.py b/tests/functional/codegen/modules/test_exports.py index 93f4fe6c2f..3cc21d61a9 100644 --- a/tests/functional/codegen/modules/test_exports.py +++ b/tests/functional/codegen/modules/test_exports.py @@ -440,3 +440,26 @@ def __init__(): # call `c.__default__()` env.message_call(c.address) assert c.counter() == 6 + + +def test_inline_interface_export(make_input_bundle, get_contract): + lib1 = """ +interface IAsset: + def asset() -> address: view + +implements: IAsset + +@external +@view +def asset() -> address: + return self + """ + main = """ +import lib1 + +exports: lib1.IAsset + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + c = get_contract(main, input_bundle=input_bundle) + + assert c.asset() == c.address diff --git a/tests/functional/codegen/modules/test_interface_imports.py b/tests/functional/codegen/modules/test_interface_imports.py index 3f0f8cb010..af9f9b5e68 100644 --- a/tests/functional/codegen/modules/test_interface_imports.py +++ b/tests/functional/codegen/modules/test_interface_imports.py @@ -1,3 +1,6 @@ +import pytest + + def test_import_interface_types(make_input_bundle, get_contract): ifaces = """ interface IFoo: @@ -50,9 +53,10 @@ def foo() -> bool: # check that this typechecks both directions a: lib1.IERC20 = IERC20(msg.sender) b: lib2.IERC20 = IERC20(msg.sender) + c: IERC20 = lib1.IERC20(msg.sender) # allowed in call position # return the equality so we can sanity check it - return a == b + return a == b and b == c """ input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2}) c = get_contract(main, input_bundle=input_bundle) @@ -60,6 +64,36 @@ def foo() -> bool: assert c.foo() is True +@pytest.mark.parametrize("interface_syntax", ["__at__", "__interface__"]) +def test_intrinsic_interface(get_contract, make_input_bundle, interface_syntax): + lib = """ +@external +@view +def foo() -> uint256: + # detect self call + if msg.sender == self: + return 4 + else: + return 5 + """ + + main = f""" +import lib + +exports: lib.__interface__ + +@external +@view +def bar() -> uint256: + return staticcall lib.{interface_syntax}(self).foo() + """ + input_bundle = make_input_bundle({"lib.vy": lib}) + c = get_contract(main, input_bundle=input_bundle) + + assert c.foo() == 5 + assert c.bar() == 4 + + def test_import_interface_flags(make_input_bundle, get_contract): ifaces = """ flag Foo: diff --git a/tests/functional/codegen/test_interfaces.py b/tests/functional/codegen/test_interfaces.py index 8887bf07cb..31475a3bc0 100644 --- a/tests/functional/codegen/test_interfaces.py +++ b/tests/functional/codegen/test_interfaces.py @@ -774,3 +774,92 @@ def foo(s: MyStruct) -> MyStruct: assert "b: uint256" in out assert "struct Voter:" in out assert "voted: bool" in out + + +def test_intrinsic_interface_instantiation(make_input_bundle, get_contract): + lib1 = """ +@external +@view +def foo(): + pass + """ + main = """ +import lib1 + +i: lib1.__interface__ + +@external +def bar() -> lib1.__interface__: + self.i = lib1.__at__(self) + return self.i + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + c = get_contract(main, input_bundle=input_bundle) + + assert c.bar() == c.address + + +def test_intrinsic_interface_converts(make_input_bundle, get_contract): + lib1 = """ +@external +@view +def foo(): + pass + """ + main = """ +import lib1 + +@external +def bar() -> lib1.__interface__: + return lib1.__at__(self) + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + c = get_contract(main, input_bundle=input_bundle) + + assert c.bar() == c.address + + +def test_intrinsic_interface_kws(env, make_input_bundle, get_contract): + value = 10**5 + lib1 = f""" +@external +@payable +def foo(a: address): + send(a, {value}) + """ + main = f""" +import lib1 + +exports: lib1.__interface__ + +@external +def bar(a: address): + extcall lib1.__at__(self).foo(a, value={value}) + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + c = get_contract(main, input_bundle=input_bundle) + env.set_balance(c.address, value) + original_balance = env.get_balance(env.deployer) + c.bar(env.deployer) + assert env.get_balance(env.deployer) == original_balance + value + + +def test_intrinsic_interface_defaults(env, make_input_bundle, get_contract): + lib1 = """ +@external +@payable +def foo(i: uint256=1) -> uint256: + return i + """ + main = """ +import lib1 + +exports: lib1.__interface__ + +@external +def bar() -> uint256: + return extcall lib1.__at__(self).foo() + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + c = get_contract(main, input_bundle=input_bundle) + assert c.bar() == 1 diff --git a/tests/functional/codegen/types/numbers/test_exponents.py b/tests/functional/codegen/types/numbers/test_exponents.py index 702cbcb1dd..28dba59edc 100644 --- a/tests/functional/codegen/types/numbers/test_exponents.py +++ b/tests/functional/codegen/types/numbers/test_exponents.py @@ -173,3 +173,17 @@ def foo(b: int128) -> int128: c.foo(max_power) with tx_failed(): c.foo(max_power + 1) + + +valid_list = [ + """ +@external +def foo() -> uint256: + return (10**18)**2 + """ +] + + +@pytest.mark.parametrize("good_code", valid_list) +def test_exponent_success(good_code): + assert compile_code(good_code) is not None diff --git a/tests/functional/syntax/exceptions/test_invalid_literal_exception.py b/tests/functional/syntax/exceptions/test_invalid_literal_exception.py index a0cf10ad02..f3fd73fbfc 100644 --- a/tests/functional/syntax/exceptions/test_invalid_literal_exception.py +++ b/tests/functional/syntax/exceptions/test_invalid_literal_exception.py @@ -36,6 +36,14 @@ def foo(): def foo(): a: bytes32 = keccak256("ั“test") """, + # test constant folding inside of `convert()` + """ +BAR: constant(uint16) = 256 + +@external +def foo(): + a: uint8 = convert(BAR, uint8) + """, ] diff --git a/tests/functional/syntax/exceptions/test_type_mismatch_exception.py b/tests/functional/syntax/exceptions/test_type_mismatch_exception.py index 76c5c481f0..63e0eb6d11 100644 --- a/tests/functional/syntax/exceptions/test_type_mismatch_exception.py +++ b/tests/functional/syntax/exceptions/test_type_mismatch_exception.py @@ -47,6 +47,14 @@ def foo(): """ a: constant(address) = 0x3cd751e6b0078be393132286c442345e5dc49699 """, + # test constant folding inside `convert()` + """ +BAR: constant(Bytes[5]) = b"vyper" + +@external +def foo(): + a: Bytes[4] = convert(BAR, Bytes[4]) + """, ] diff --git a/tests/functional/syntax/modules/test_deploy_visibility.py b/tests/functional/syntax/modules/test_deploy_visibility.py index f51bf9575b..c908d4adae 100644 --- a/tests/functional/syntax/modules/test_deploy_visibility.py +++ b/tests/functional/syntax/modules/test_deploy_visibility.py @@ -1,7 +1,7 @@ import pytest from vyper.compiler import compile_code -from vyper.exceptions import CallViolation +from vyper.exceptions import CallViolation, UnknownAttribute def test_call_deploy_from_external(make_input_bundle): @@ -25,3 +25,35 @@ def foo(): compile_code(main, input_bundle=input_bundle) assert e.value.message == "Cannot call an @deploy function from an @external function!" + + +@pytest.mark.parametrize("interface_syntax", ["__interface__", "__at__"]) +def test_module_interface_init(make_input_bundle, tmp_path, interface_syntax): + lib1 = """ +#lib1.vy +k: uint256 + +@external +def bar(): + pass + +@deploy +def __init__(): + self.k = 10 + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + + code = f""" +import lib1 + +@deploy +def __init__(): + lib1.{interface_syntax}(self).__init__() + """ + + with pytest.raises(UnknownAttribute) as e: + compile_code(code, input_bundle=input_bundle) + + # as_posix() for windows tests + lib1_path = (tmp_path / "lib1.vy").as_posix() + assert e.value.message == f"interface {lib1_path} has no member '__init__'." diff --git a/tests/functional/syntax/modules/test_exports.py b/tests/functional/syntax/modules/test_exports.py index 7b00d29c98..4314c1bbf0 100644 --- a/tests/functional/syntax/modules/test_exports.py +++ b/tests/functional/syntax/modules/test_exports.py @@ -385,6 +385,28 @@ def do_xyz(): assert e.value._message == "requested `lib1.ifoo` but `lib1` does not implement `lib1.ifoo`!" +def test_no_export_unimplemented_inline_interface(make_input_bundle): + lib1 = """ +interface ifoo: + def do_xyz(): nonpayable + +# technically implements ifoo, but missing `implements: ifoo` + +@external +def do_xyz(): + pass + """ + main = """ +import lib1 + +exports: lib1.ifoo + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + with pytest.raises(InterfaceViolation) as e: + compile_code(main, input_bundle=input_bundle) + assert e.value._message == "requested `lib1.ifoo` but `lib1` does not implement `lib1.ifoo`!" + + def test_export_selector_conflict(make_input_bundle): ifoo = """ @external @@ -444,3 +466,87 @@ def __init__(): with pytest.raises(InterfaceViolation) as e: compile_code(main, input_bundle=input_bundle) assert e.value._message == "requested `lib1.ifoo` but `lib1` does not implement `lib1.ifoo`!" + + +def test_export_empty_interface(make_input_bundle, tmp_path): + lib1 = """ +def an_internal_function(): + pass + """ + main = """ +import lib1 + +exports: lib1.__interface__ + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + with pytest.raises(StructureException) as e: + compile_code(main, input_bundle=input_bundle) + + # as_posix() for windows + lib1_path = (tmp_path / "lib1.vy").as_posix() + assert e.value._message == f"lib1 (located at `{lib1_path}`) has no external functions!" + + +def test_invalid_export(make_input_bundle): + lib1 = """ +@external +def foo(): + pass + """ + main = """ +import lib1 +a: address + +exports: lib1.__interface__(self.a).foo + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + + with pytest.raises(StructureException) as e: + compile_code(main, input_bundle=input_bundle) + + assert e.value._message == "invalid export of a value" + assert e.value._hint == "exports should look like ." + + main = """ +interface Foo: + def foo(): nonpayable + +exports: Foo + """ + with pytest.raises(StructureException) as e: + compile_code(main) + + assert e.value._message == "invalid export" + assert e.value._hint == "exports should look like ." + + +@pytest.mark.parametrize("exports_item", ["__at__", "__at__(self)", "__at__(self).__interface__"]) +def test_invalid_at_exports(get_contract, make_input_bundle, exports_item): + lib = """ +@external +@view +def foo() -> uint256: + return 5 + """ + + main = f""" +import lib + +exports: lib.{exports_item} + +@external +@view +def bar() -> uint256: + return staticcall lib.__at__(self).foo() + """ + input_bundle = make_input_bundle({"lib.vy": lib}) + + with pytest.raises(Exception) as e: + compile_code(main, input_bundle=input_bundle) + + if exports_item == "__at__": + assert "not a function or interface" in str(e.value) + if exports_item == "__at__(self)": + assert "invalid exports" in str(e.value) + if exports_item == "__at__(self).__interface__": + assert "has no member '__interface__'" in str(e.value) diff --git a/tests/functional/syntax/test_abi_encode.py b/tests/functional/syntax/test_abi_encode.py index 5e0175857d..edb441652a 100644 --- a/tests/functional/syntax/test_abi_encode.py +++ b/tests/functional/syntax/test_abi_encode.py @@ -1,7 +1,7 @@ import pytest from vyper import compiler -from vyper.exceptions import TypeMismatch +from vyper.exceptions import InvalidLiteral, TypeMismatch fail_list = [ ( @@ -41,11 +41,37 @@ def foo(x: uint256) -> Bytes[36]: ( """ @external +def foo(x: uint256) -> Bytes[36]: + return _abi_encode(x, method_id=b"abc") + """, + InvalidLiteral, # len(method_id) must be greater than 3 + ), + ( + """ +@external def foo(x: uint256) -> Bytes[36]: return _abi_encode(x, method_id=0x1234567890) """, TypeMismatch, # len(method_id) must be less than 4 ), + ( + """ +@external +def foo(x: uint256) -> Bytes[36]: + return _abi_encode(x, method_id=0x123456) + """, + TypeMismatch, # len(method_id) must be greater than 3 + ), + ( + """ +@external +def foo() -> Bytes[132]: + x: uint256 = 1 + y: Bytes[32] = b"234" + return abi_encode(x, y, method_id=b"") + """, + InvalidLiteral, # len(method_id) must be 4 + ), ] @@ -82,6 +108,11 @@ def foo(x: Bytes[1]) -> Bytes[68]: return _abi_encode(x, ensure_tuple=False, method_id=0x12345678) """, """ +@external +def foo(x: Bytes[1]) -> Bytes[68]: + return _abi_encode(x, ensure_tuple=False, method_id=b"1234") + """, + """ BAR: constant(DynArray[uint256, 5]) = [1, 2, 3, 4, 5] @external diff --git a/tests/functional/syntax/test_interfaces.py b/tests/functional/syntax/test_interfaces.py index 86ea4bcfd0..baf0c73c30 100644 --- a/tests/functional/syntax/test_interfaces.py +++ b/tests/functional/syntax/test_interfaces.py @@ -571,3 +571,53 @@ def bar(): compiler.compile_code(code, input_bundle=input_bundle) assert e.value.message == "Contract does not implement all interface functions: bar(), foobar()" + + +def test_intrinsic_interfaces_different_types(make_input_bundle, get_contract): + lib1 = """ +@external +@view +def foo(): + pass + """ + lib2 = """ +@external +@view +def foo(): + pass + """ + main = """ +import lib1 +import lib2 + +@external +def bar(): + assert lib1.__at__(self) == lib2.__at__(self) + """ + input_bundle = make_input_bundle({"lib1.vy": lib1, "lib2.vy": lib2}) + + with pytest.raises(TypeMismatch): + compiler.compile_code(main, input_bundle=input_bundle) + + +@pytest.mark.xfail +def test_intrinsic_interfaces_default_function(make_input_bundle, get_contract): + lib1 = """ +@external +@payable +def __default__(): + pass + """ + main = """ +import lib1 + +@external +def bar(): + extcall lib1.__at__(self).__default__() + + """ + input_bundle = make_input_bundle({"lib1.vy": lib1}) + + # TODO make the exception more precise once fixed + with pytest.raises(Exception): # noqa: B017 + compiler.compile_code(main, input_bundle=input_bundle) diff --git a/tests/functional/syntax/test_slice.py b/tests/functional/syntax/test_slice.py index 6bb666527e..6a091c9da3 100644 --- a/tests/functional/syntax/test_slice.py +++ b/tests/functional/syntax/test_slice.py @@ -53,6 +53,22 @@ def foo(inp: Bytes[10]) -> Bytes[4]: def foo() -> Bytes[10]: return slice(b"badmintonzzz", 1, 10) """, + # test constant folding for `slice()` `length` argument + """ +@external +def foo(): + x: Bytes[32] = slice(msg.data, 0, 31 + 1) + """, + """ +@external +def foo(a: address): + x: Bytes[32] = slice(a.code, 0, 31 + 1) + """, + """ +@external +def foo(inp: Bytes[5], start: uint256) -> Bytes[3]: + return slice(inp, 0, 1 + 1) + """, ] diff --git a/vyper/builtins/_convert.py b/vyper/builtins/_convert.py index aa53dee429..a494e4a344 100644 --- a/vyper/builtins/_convert.py +++ b/vyper/builtins/_convert.py @@ -463,7 +463,7 @@ def to_flag(expr, arg, out_typ): def convert(expr, context): assert len(expr.args) == 2, "bad typecheck: convert" - arg_ast = expr.args[0] + arg_ast = expr.args[0].reduced() arg = Expr(arg_ast, context).ir_node original_arg = arg diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index 674efda7ce..62539872bc 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -305,7 +305,7 @@ def fetch_call_return(self, node): arg = node.args[0] start_expr = node.args[1] - length_expr = node.args[2] + length_expr = node.args[2].reduced() # CMC 2022-03-22 NOTE slight code duplication with semantics/analysis/local is_adhoc_slice = arg.get("attr") == "code" or ( @@ -1257,7 +1257,8 @@ def fetch_call_return(self, node): def infer_arg_types(self, node, expected_return_typ=None): self._validate_arg_types(node) - if not isinstance(node.args[0], vy_ast.List) or len(node.args[0].elements) > 4: + arg = node.args[0].reduced() + if not isinstance(arg, vy_ast.List) or len(arg.elements) > 4: raise InvalidType("Expecting a list of 0-4 topics as first argument", node.args[0]) # return a concrete type for `data` @@ -1269,7 +1270,7 @@ def infer_arg_types(self, node, expected_return_typ=None): def build_IR(self, expr, args, kwargs, context): context.check_is_not_constant(f"use {self._id}", expr) - topics_length = len(expr.args[0].elements) + topics_length = len(expr.args[0].reduced().elements) topics = args[0].args topics = [unwrap_location(topic) for topic in topics] @@ -2166,10 +2167,9 @@ def build_IR(self, expr, args, kwargs, context): variables_2=variables_2, memory_allocator=context.memory_allocator, ) + z_ir = new_ctx.vars["z"].as_ir_node() ret = IRnode.from_list( - ["seq", placeholder_copy, sqrt_ir, new_ctx.vars["z"].pos], # load x variable - typ=DecimalT(), - location=MEMORY, + ["seq", placeholder_copy, sqrt_ir, z_ir], typ=DecimalT(), location=MEMORY ) return b1.resolve(ret) @@ -2363,7 +2363,13 @@ def infer_kwarg_types(self, node): for kwarg in node.keywords: kwarg_name = kwarg.arg validate_expected_type(kwarg.value, self._kwargs[kwarg_name].typ) - ret[kwarg_name] = get_exact_type_from_node(kwarg.value) + + typ = get_exact_type_from_node(kwarg.value) + if kwarg_name == "method_id" and isinstance(typ, BytesT): + if typ.length != 4: + raise InvalidLiteral("method_id must be exactly 4 bytes!", kwarg.value) + + ret[kwarg_name] = typ return ret def fetch_call_return(self, node): diff --git a/vyper/cli/compile_archive.py b/vyper/cli/compile_archive.py index 1b52343c1c..c6d07de9f1 100644 --- a/vyper/cli/compile_archive.py +++ b/vyper/cli/compile_archive.py @@ -8,8 +8,9 @@ import zipfile from pathlib import PurePath -from vyper.compiler import compile_from_file_input +from vyper.compiler import outputs_from_compiler_data from vyper.compiler.input_bundle import FileInput, ZipInputBundle +from vyper.compiler.phases import CompilerData from vyper.compiler.settings import Settings, merge_settings from vyper.exceptions import BadArchive @@ -19,6 +20,11 @@ class NotZipInput(Exception): def compile_from_zip(file_name, output_formats, settings, no_bytecode_metadata): + compiler_data = compiler_data_from_zip(file_name, settings, no_bytecode_metadata) + return outputs_from_compiler_data(compiler_data, output_formats) + + +def compiler_data_from_zip(file_name, settings, no_bytecode_metadata): with open(file_name, "rb") as f: bcontents = f.read() @@ -59,11 +65,9 @@ def compile_from_zip(file_name, output_formats, settings, no_bytecode_metadata): settings, archive_settings, lhs_source="command line", rhs_source="archive settings" ) - # TODO: validate integrity sum (probably in CompilerData) - return compile_from_file_input( + return CompilerData( file, input_bundle=input_bundle, - output_formats=output_formats, integrity_sum=integrity, settings=settings, no_bytecode_metadata=no_bytecode_metadata, diff --git a/vyper/codegen/context.py b/vyper/codegen/context.py index f49914ac78..7995b7b9f5 100644 --- a/vyper/codegen/context.py +++ b/vyper/codegen/context.py @@ -15,6 +15,17 @@ class Constancy(enum.Enum): Constant = 1 +_alloca_id = 0 + + +def _generate_alloca_id(): + # note: this gets reset between compiler runs by codegen.core.reset_names + global _alloca_id + + _alloca_id += 1 + return _alloca_id + + @dataclass(frozen=True) class Alloca: name: str @@ -22,6 +33,8 @@ class Alloca: typ: VyperType size: int + _id: int + def __post_init__(self): assert self.typ.memory_bytes_required == self.size @@ -233,7 +246,9 @@ def _new_variable( pos = f"$palloca_{ofst}_{size}" else: pos = f"$alloca_{ofst}_{size}" - alloca = Alloca(name=name, offset=ofst, typ=typ, size=size) + + alloca_id = _generate_alloca_id() + alloca = Alloca(name=name, offset=ofst, typ=typ, size=size, _id=alloca_id) var = VariableRecord( name=name, diff --git a/vyper/codegen/core.py b/vyper/codegen/core.py index 2bd4f81f50..0ad7fa79c6 100644 --- a/vyper/codegen/core.py +++ b/vyper/codegen/core.py @@ -1,3 +1,4 @@ +import vyper.codegen.context as ctx from vyper.codegen.ir_node import Encoding, IRnode from vyper.compiler.settings import _opt_codesize, _opt_gas, _opt_none from vyper.evm.address_space import ( @@ -855,6 +856,9 @@ def reset_names(): global _label _label = 0 + # could be refactored + ctx._alloca_id = 0 + # returns True if t is ABI encoded and is a type that needs any kind of # validation diff --git a/vyper/codegen/expr.py b/vyper/codegen/expr.py index cd51966710..3a09bbe6c0 100644 --- a/vyper/codegen/expr.py +++ b/vyper/codegen/expr.py @@ -51,6 +51,7 @@ FlagT, HashMapT, InterfaceT, + ModuleT, SArrayT, StringT, StructT, @@ -680,7 +681,8 @@ def parse_Call(self): # TODO fix cyclic import from vyper.builtins._signatures import BuiltinFunctionT - func_t = self.expr.func._metadata["type"] + func = self.expr.func + func_t = func._metadata["type"] if isinstance(func_t, BuiltinFunctionT): return func_t.build_IR(self.expr, self.context) @@ -691,8 +693,14 @@ def parse_Call(self): return self.handle_struct_literal() # Interface constructor. Bar(
). - if is_type_t(func_t, InterfaceT): + if is_type_t(func_t, InterfaceT) or func.get("attr") == "__at__": assert not self.is_stmt # sanity check typechecker + + # magic: do sanity checks for module.__at__ + if func.get("attr") == "__at__": + assert isinstance(func_t, MemberFunctionT) + assert isinstance(func.value._metadata["type"], ModuleT) + (arg0,) = self.expr.args arg_ir = Expr(arg0, self.context).ir_node @@ -702,16 +710,16 @@ def parse_Call(self): return arg_ir if isinstance(func_t, MemberFunctionT): - darray = Expr(self.expr.func.value, self.context).ir_node + # TODO consider moving these to builtins or a dedicated file + darray = Expr(func.value, self.context).ir_node assert isinstance(darray.typ, DArrayT) args = [Expr(x, self.context).ir_node for x in self.expr.args] - if self.expr.func.attr == "pop": - # TODO consider moving this to builtins - darray = Expr(self.expr.func.value, self.context).ir_node + if func.attr == "pop": + darray = Expr(func.value, self.context).ir_node assert len(self.expr.args) == 0 return_item = not self.is_stmt return pop_dyn_array(darray, return_popped_item=return_item) - elif self.expr.func.attr == "append": + elif func.attr == "append": (arg,) = args check_assign( dummy_node_for_type(darray.typ.value_type), dummy_node_for_type(arg.typ) @@ -726,6 +734,8 @@ def parse_Call(self): ret.append(append_dyn_array(darray, arg)) return IRnode.from_list(ret) + raise CompilerPanic("unreachable!") # pragma: nocover + assert isinstance(func_t, ContractFunctionT) assert func_t.is_internal or func_t.is_constructor diff --git a/vyper/compiler/__init__.py b/vyper/compiler/__init__.py index 0345c24931..d885599cec 100644 --- a/vyper/compiler/__init__.py +++ b/vyper/compiler/__init__.py @@ -99,13 +99,6 @@ def compile_from_file_input( """ settings = settings or get_global_settings() or Settings() - if output_formats is None: - output_formats = ("bytecode",) - - # make IR output the same between runs - # TODO: move this to CompilerData.__init__() - codegen.reset_names() - compiler_data = CompilerData( file_input, input_bundle, @@ -116,6 +109,17 @@ def compile_from_file_input( no_bytecode_metadata=no_bytecode_metadata, ) + return outputs_from_compiler_data(compiler_data, output_formats, exc_handler) + + +def outputs_from_compiler_data( + compiler_data: CompilerData, + output_formats: Optional[OutputFormats] = None, + exc_handler: Optional[Callable] = None, +): + if output_formats is None: + output_formats = ("bytecode",) + ret = {} with anchor_settings(compiler_data.settings): for output_format in output_formats: @@ -126,7 +130,7 @@ def compile_from_file_input( ret[output_format] = formatter(compiler_data) except Exception as exc: if exc_handler is not None: - exc_handler(str(file_input.path), exc) + exc_handler(str(compiler_data.file_input.path), exc) else: raise exc diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py index bcbd0a1578..201e1e2af6 100644 --- a/vyper/compiler/output.py +++ b/vyper/compiler/output.py @@ -282,6 +282,9 @@ def build_abi_output(compiler_data: CompilerData) -> list: _ = compiler_data.ir_runtime # ensure _ir_info is generated abi = module_t.interface.to_toplevel_abi_dict() + if module_t.init_function: + abi += module_t.init_function.to_toplevel_abi_dict() + if compiler_data.show_gas_estimates: # Add gas estimates for each function to ABI gas_estimates = build_gas_estimates(compiler_data.function_signatures) diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index d9b6b13b48..503281a867 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -4,6 +4,7 @@ from pathlib import Path, PurePath from typing import Any, Optional +import vyper.codegen.core as codegen from vyper import ast as vy_ast from vyper.ast import natspec from vyper.codegen import module @@ -304,6 +305,9 @@ def generate_ir_nodes(global_ctx: ModuleT, settings: Settings) -> tuple[IRnode, IR to generate deployment bytecode IR to generate runtime bytecode """ + # make IR output the same between runs + codegen.reset_names() + with anchor_settings(settings): ir_nodes, ir_runtime = module.generate_ir_for_module(global_ctx) if settings.optimize != OptimizationLevel.NONE: diff --git a/vyper/semantics/analysis/base.py b/vyper/semantics/analysis/base.py index e275930fa0..adfc7540a0 100644 --- a/vyper/semantics/analysis/base.py +++ b/vyper/semantics/analysis/base.py @@ -96,6 +96,7 @@ class AnalysisResult: class ModuleInfo(AnalysisResult): module_t: "ModuleT" alias: str + # import_node: vy_ast._ImportStmt # maybe could be useful ownership: ModuleOwnership = ModuleOwnership.NO_OWNERSHIP ownership_decl: Optional[vy_ast.VyperNode] = None diff --git a/vyper/semantics/analysis/local.py b/vyper/semantics/analysis/local.py index 809c6532c6..461326d72d 100644 --- a/vyper/semantics/analysis/local.py +++ b/vyper/semantics/analysis/local.py @@ -136,7 +136,7 @@ def _validate_address_code(node: vy_ast.Attribute, value_type: VyperType) -> Non parent = node.get_ancestor() if isinstance(parent, vy_ast.Call): ok_func = isinstance(parent.func, vy_ast.Name) and parent.func.id == "slice" - ok_args = len(parent.args) == 3 and isinstance(parent.args[2], vy_ast.Int) + ok_args = len(parent.args) == 3 and isinstance(parent.args[2].reduced(), vy_ast.Int) if ok_func and ok_args: return @@ -154,7 +154,7 @@ def _validate_msg_data_attribute(node: vy_ast.Attribute) -> None: "msg.data is only allowed inside of the slice, len or raw_call functions", node ) if parent.get("func.id") == "slice": - ok_args = len(parent.args) == 3 and isinstance(parent.args[2], vy_ast.Int) + ok_args = len(parent.args) == 3 and isinstance(parent.args[2].reduced(), vy_ast.Int) if not ok_args: raise StructureException( "slice(msg.data) must use a compile-time constant for length argument", parent diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index 8a2beb61e6..737f675b7c 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -40,7 +40,7 @@ ) from vyper.semantics.data_locations import DataLocation from vyper.semantics.namespace import Namespace, get_namespace, override_global_namespace -from vyper.semantics.types import EventT, FlagT, InterfaceT, StructT +from vyper.semantics.types import TYPE_T, EventT, FlagT, InterfaceT, StructT, is_type_t from vyper.semantics.types.function import ContractFunctionT from vyper.semantics.types.module import ModuleT from vyper.semantics.types.utils import type_from_annotation @@ -499,9 +499,19 @@ def visit_ExportsDecl(self, node): raise StructureException("not a public variable!", decl, item) funcs = [decl._expanded_getter._metadata["func_type"]] elif isinstance(info.typ, ContractFunctionT): + # e.g. lib1.__interface__(self._addr).foo + if not isinstance(get_expr_info(item.value).typ, (ModuleT, TYPE_T)): + raise StructureException( + "invalid export of a value", + item.value, + hint="exports should look like .", + ) + # regular function funcs = [info.typ] - elif isinstance(info.typ, InterfaceT): + elif is_type_t(info.typ, InterfaceT): + interface_t = info.typ.typedef + if not isinstance(item, vy_ast.Attribute): raise StructureException( "invalid export", @@ -512,7 +522,7 @@ def visit_ExportsDecl(self, node): if module_info is None: raise StructureException("not a valid module!", item.value) - if info.typ not in module_info.typ.implemented_interfaces: + if interface_t not in module_info.typ.implemented_interfaces: iface_str = item.node_source_code module_str = item.value.node_source_code msg = f"requested `{iface_str}` but `{module_str}`" @@ -523,9 +533,15 @@ def visit_ExportsDecl(self, node): # find the specific implementation of the function in the module funcs = [ module_exposed_fns[fn.name] - for fn in info.typ.functions.values() + for fn in interface_t.functions.values() if fn.is_external ] + + if len(funcs) == 0: + path = module_info.module_node.path + msg = f"{module_info.alias} (located at `{path}`) has no external functions!" + raise StructureException(msg, item) + else: raise StructureException( f"not a function or interface: `{info.typ}`", info.typ.decl_node, item diff --git a/vyper/semantics/analysis/utils.py b/vyper/semantics/analysis/utils.py index 9734087fc3..8727f3750d 100644 --- a/vyper/semantics/analysis/utils.py +++ b/vyper/semantics/analysis/utils.py @@ -41,7 +41,7 @@ def _validate_op(node, types_list, validation_fn_name): try: _validate_fn(node) ret.append(type_) - except InvalidOperation as e: + except (InvalidOperation, OverflowException) as e: err_list.append(e) if ret: @@ -199,7 +199,7 @@ def _raise_invalid_reference(name, node): try: s = t.get_member(name, node) - if isinstance(s, (VyperType, TYPE_T)): + if isinstance(s, VyperType): # ex. foo.bar(). bar() is a ContractFunctionT return [s] diff --git a/vyper/semantics/types/__init__.py b/vyper/semantics/types/__init__.py index 59a20dd99f..b881f52b2b 100644 --- a/vyper/semantics/types/__init__.py +++ b/vyper/semantics/types/__init__.py @@ -1,8 +1,8 @@ from . import primitives, subscriptable, user from .base import TYPE_T, VOID_TYPE, KwargSettings, VyperType, is_type_t, map_void from .bytestrings import BytesT, StringT, _BytestringT -from .function import MemberFunctionT -from .module import InterfaceT +from .function import ContractFunctionT, MemberFunctionT +from .module import InterfaceT, ModuleT from .primitives import AddressT, BoolT, BytesM_T, DecimalT, IntegerT, SelfT from .subscriptable import DArrayT, HashMapT, SArrayT, TupleT from .user import EventT, FlagT, StructT diff --git a/vyper/semantics/types/base.py b/vyper/semantics/types/base.py index 128ede0d5b..aca37b33a3 100644 --- a/vyper/semantics/types/base.py +++ b/vyper/semantics/types/base.py @@ -114,8 +114,13 @@ def __eq__(self, other): ) def __lt__(self, other): + # CMC 2024-10-20 what is this for? return self.abi_type.selector_name() < other.abi_type.selector_name() + def __repr__(self): + # TODO: add `pretty()` to the VyperType API? + return self._id + # return a dict suitable for serializing in the AST def to_dict(self): ret = {"name": self._id} @@ -362,10 +367,7 @@ def get_member(self, key: str, node: vy_ast.VyperNode) -> "VyperType": raise StructureException(f"{self} instance does not have members", node) hint = get_levenshtein_error_suggestions(key, self.members, 0.3) - raise UnknownAttribute(f"{self} has no member '{key}'.", node, hint=hint) - - def __repr__(self): - return self._id + raise UnknownAttribute(f"{repr(self)} has no member '{key}'.", node, hint=hint) class KwargSettings: diff --git a/vyper/semantics/types/function.py b/vyper/semantics/types/function.py index 7a56b01281..ffeb5b7299 100644 --- a/vyper/semantics/types/function.py +++ b/vyper/semantics/types/function.py @@ -874,7 +874,7 @@ def _id(self): return self.name def __repr__(self): - return f"{self.underlying_type._id} member function '{self.name}'" + return f"{self.underlying_type} member function '{self.name}'" def fetch_call_return(self, node: vy_ast.Call) -> Optional[VyperType]: validate_call_args(node, len(self.arg_types)) diff --git a/vyper/semantics/types/module.py b/vyper/semantics/types/module.py index dabeaf21b6..498757b94e 100644 --- a/vyper/semantics/types/module.py +++ b/vyper/semantics/types/module.py @@ -19,7 +19,7 @@ ) from vyper.semantics.data_locations import DataLocation from vyper.semantics.types.base import TYPE_T, VyperType, is_type_t -from vyper.semantics.types.function import ContractFunctionT +from vyper.semantics.types.function import ContractFunctionT, MemberFunctionT from vyper.semantics.types.primitives import AddressT from vyper.semantics.types.user import EventT, FlagT, StructT, _UserType from vyper.utils import OrderedSet @@ -240,9 +240,6 @@ def from_ModuleT(cls, module_t: "ModuleT") -> "InterfaceT": for fn_t in module_t.exposed_functions: funcs.append((fn_t.name, fn_t)) - if (fn_t := module_t.init_function) is not None: - funcs.append((fn_t.name, fn_t)) - event_set: OrderedSet[EventT] = OrderedSet() event_set.update([node._metadata["event_type"] for node in module_t.event_defs]) event_set.update(module_t.used_events) @@ -273,6 +270,19 @@ def from_InterfaceDef(cls, node: vy_ast.InterfaceDef) -> "InterfaceT": return cls._from_lists(node.name, node, functions) +def _module_at(module_t): + return MemberFunctionT( + # set underlying_type to a TYPE_T as a bit of a kludge, since it's + # kind of like a class method (but we don't have classmethod + # abstraction) + underlying_type=TYPE_T(module_t), + name="__at__", + arg_types=[AddressT()], + return_type=module_t.interface, + is_modifying=False, + ) + + # Datatype to store all module information. class ModuleT(VyperType): typeclass = "module" @@ -330,16 +340,28 @@ def __init__(self, module: vy_ast.Module, name: Optional[str] = None): for i in self.import_stmts: import_info = i._metadata["import_info"] - self.add_member(import_info.alias, import_info.typ) if hasattr(import_info.typ, "module_t"): - self._helper.add_member(import_info.alias, TYPE_T(import_info.typ)) + module_info = import_info.typ + # get_expr_info uses ModuleInfo + self.add_member(import_info.alias, module_info) + # type_from_annotation uses TYPE_T + self._helper.add_member(import_info.alias, TYPE_T(module_info.module_t)) + else: # interfaces + assert isinstance(import_info.typ, InterfaceT) + self.add_member(import_info.alias, TYPE_T(import_info.typ)) for name, interface_t in self.interfaces.items(): # can access interfaces in type position self._helper.add_member(name, TYPE_T(interface_t)) - self.add_member("__interface__", self.interface) + # module.__at__(addr) + self.add_member("__at__", _module_at(self)) + + # allow `module.__interface__` (in exports declarations) + self.add_member("__interface__", TYPE_T(self.interface)) + # allow `module.__interface__` (in type position) + self._helper.add_member("__interface__", TYPE_T(self.interface)) # __eq__ is very strict on ModuleT - object equality! this is because we # don't want to reason about where a module came from (i.e. input bundle, diff --git a/vyper/semantics/types/primitives.py b/vyper/semantics/types/primitives.py index 5c0362e662..dcc4fe8c8e 100644 --- a/vyper/semantics/types/primitives.py +++ b/vyper/semantics/types/primitives.py @@ -173,11 +173,11 @@ def _get_lr(): if isinstance(left, vy_ast.Int): if left.value >= 2**value_bits: raise OverflowException( - "Base is too large, calculation will always overflow", left + f"Base is too large for {self}, calculation will always overflow", left ) elif left.value < -(2**value_bits): raise OverflowException( - "Base is too small, calculation will always underflow", left + f"Base is too small for {self}, calculation will always underflow", left ) elif isinstance(right, vy_ast.Int): if right.value < 0: diff --git a/vyper/venom/README.md b/vyper/venom/README.md index 6f3b318c9b..ea6eabebaa 100644 --- a/vyper/venom/README.md +++ b/vyper/venom/README.md @@ -209,15 +209,16 @@ Assembly can be inspected with `-f asm`, whereas an opcode view of the final byt - Effectively translates to `JUMP`, and marks the call site as a valid return destination (for callee to jump back to) by `JUMPDEST`. - `alloca` - ``` - out = alloca size, offset + out = alloca size, offset, id ``` - Allocates memory of a given `size` at a given `offset` in memory. + - The `id` argument is there to help debugging translation into venom - The output is the offset value itself. - Because the SSA form does not allow changing values of registers, handling mutable variables can be tricky. The `alloca` instruction is meant to simplify that. - `palloca` - ``` - out = palloca size, offset + out = palloca size, offset, id ``` - Like the `alloca` instruction but only used for parameters of internal functions which are passed by memory. - `iload` diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index bf3115b4dd..593a9556a9 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -14,6 +14,7 @@ AlgebraicOptimizationPass, BranchOptimizationPass, DFTPass, + FloatAllocas, MakeSSA, Mem2Var, RemoveUnusedVariablesPass, @@ -47,6 +48,8 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: ac = IRAnalysesCache(fn) + FloatAllocas(ac, fn).run_pass() + SimplifyCFGPass(ac, fn).run_pass() MakeSSA(ac, fn).run_pass() Mem2Var(ac, fn).run_pass() diff --git a/vyper/venom/effects.py b/vyper/venom/effects.py index a668ff5439..97cffe2cb2 100644 --- a/vyper/venom/effects.py +++ b/vyper/venom/effects.py @@ -68,6 +68,8 @@ def __iter__(self): "balance": BALANCE, "selfbalance": BALANCE, "extcodecopy": EXTCODE, + "extcodesize": EXTCODE, + "extcodehash": EXTCODE, "selfdestruct": BALANCE, # may modify code, but after the transaction "log": MEMORY, "revert": MEMORY, diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 02a9f4d1f7..782309d841 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -107,18 +107,16 @@ NOOP_INSTRUCTIONS = frozenset(["pass", "cleanup_repeat", "var_list", "unique_symbol"]) SymbolTable = dict[str, Optional[IROperand]] -_global_symbols: SymbolTable = None # type: ignore +_alloca_table: SymbolTable = None # type: ignore MAIN_ENTRY_LABEL_NAME = "__main_entry" -_external_functions: dict[int, SymbolTable] = None # type: ignore # convert IRnode directly to venom def ir_node_to_venom(ir: IRnode) -> IRContext: _ = ir.unique_symbols # run unique symbols check - global _global_symbols, _external_functions - _global_symbols = {} - _external_functions = {} + global _alloca_table + _alloca_table = {} ctx = IRContext() fn = ctx.create_function(MAIN_ENTRY_LABEL_NAME) @@ -233,7 +231,7 @@ def pop_source(*args, **kwargs): def _convert_ir_bb(fn, ir, symbols): assert isinstance(ir, IRnode), ir # TODO: refactor these to not be globals - global _break_target, _continue_target, _global_symbols, _external_functions + global _break_target, _continue_target, _alloca_table # keep a map from external functions to all possible entry points @@ -269,8 +267,8 @@ def _convert_ir_bb(fn, ir, symbols): if is_internal or len(re.findall(r"external.*__init__\(.*_deploy", current_func)) > 0: # Internal definition var_list = ir.args[0].args[1] + assert var_list.value == "var_list" does_return_data = IRnode.from_list(["return_buffer"]) in var_list.args - _global_symbols = {} symbols = {} new_fn = _handle_internal_func(fn, ir, does_return_data, symbols) for ir_node in ir.args[1:]: @@ -298,8 +296,6 @@ def _convert_ir_bb(fn, ir, symbols): cont_ret = _convert_ir_bb(fn, cond, symbols) cond_block = fn.get_basic_block() - saved_global_symbols = _global_symbols.copy() - then_block = IRBasicBlock(ctx.get_next_label("then"), fn) else_block = IRBasicBlock(ctx.get_next_label("else"), fn) @@ -314,7 +310,6 @@ def _convert_ir_bb(fn, ir, symbols): # convert "else" cond_symbols = symbols.copy() - _global_symbols = saved_global_symbols.copy() fn.append_basic_block(else_block) else_ret_val = None if len(ir.args) == 3: @@ -343,8 +338,6 @@ def _convert_ir_bb(fn, ir, symbols): if not then_block_finish.is_terminated: then_block_finish.append_instruction("jmp", exit_bb.label) - _global_symbols = saved_global_symbols - return if_ret elif ir.value == "with": @@ -385,13 +378,6 @@ def _convert_ir_bb(fn, ir, symbols): data = _convert_ir_bb(fn, c, symbols) ctx.append_data("db", [data]) # type: ignore elif ir.value == "label": - function_id_pattern = r"external (\d+)" - function_name = ir.args[0].value - m = re.match(function_id_pattern, function_name) - if m is not None: - function_id = m.group(1) - _global_symbols = _external_functions.setdefault(function_id, {}) - label = IRLabel(ir.args[0].value, True) bb = fn.get_basic_block() if not bb.is_terminated: @@ -463,13 +449,11 @@ def _convert_ir_bb(fn, ir, symbols): elif ir.value == "repeat": def emit_body_blocks(): - global _break_target, _continue_target, _global_symbols + global _break_target, _continue_target old_targets = _break_target, _continue_target _break_target, _continue_target = exit_block, incr_block - saved_global_symbols = _global_symbols.copy() _convert_ir_bb(fn, body, symbols.copy()) _break_target, _continue_target = old_targets - _global_symbols = saved_global_symbols sym = ir.args[0] start, end, _ = _convert_ir_bb_list(fn, ir.args[1:4], symbols) @@ -540,16 +524,25 @@ def emit_body_blocks(): elif isinstance(ir.value, str) and ir.value.upper() in get_opcodes(): _convert_ir_opcode(fn, ir, symbols) elif isinstance(ir.value, str): - if ir.value.startswith("$alloca") and ir.value not in _global_symbols: + if ir.value.startswith("$alloca"): alloca = ir.passthrough_metadata["alloca"] - ptr = fn.get_basic_block().append_instruction("alloca", alloca.offset, alloca.size) - _global_symbols[ir.value] = ptr - elif ir.value.startswith("$palloca") and ir.value not in _global_symbols: + if alloca._id not in _alloca_table: + ptr = fn.get_basic_block().append_instruction( + "alloca", alloca.offset, alloca.size, alloca._id + ) + _alloca_table[alloca._id] = ptr + return _alloca_table[alloca._id] + + elif ir.value.startswith("$palloca"): alloca = ir.passthrough_metadata["alloca"] - ptr = fn.get_basic_block().append_instruction("palloca", alloca.offset, alloca.size) - _global_symbols[ir.value] = ptr - - return _global_symbols.get(ir.value) or symbols.get(ir.value) + if alloca._id not in _alloca_table: + ptr = fn.get_basic_block().append_instruction( + "palloca", alloca.offset, alloca.size, alloca._id + ) + _alloca_table[alloca._id] = ptr + return _alloca_table[alloca._id] + + return symbols.get(ir.value) elif ir.is_literal: return IRLiteral(ir.value) else: diff --git a/vyper/venom/passes/__init__.py b/vyper/venom/passes/__init__.py index 83098234c1..fcd2aa1f22 100644 --- a/vyper/venom/passes/__init__.py +++ b/vyper/venom/passes/__init__.py @@ -1,6 +1,7 @@ from .algebraic_optimization import AlgebraicOptimizationPass from .branch_optimization import BranchOptimizationPass from .dft import DFTPass +from .float_allocas import FloatAllocas from .make_ssa import MakeSSA from .mem2var import Mem2Var from .normalization import NormalizationPass diff --git a/vyper/venom/passes/float_allocas.py b/vyper/venom/passes/float_allocas.py new file mode 100644 index 0000000000..81fa115645 --- /dev/null +++ b/vyper/venom/passes/float_allocas.py @@ -0,0 +1,36 @@ +from vyper.venom.passes.base_pass import IRPass + + +class FloatAllocas(IRPass): + """ + This pass moves allocas to the entry basic block of a function + We could probably move them to the immediate dominator of the basic + block defining the alloca instead of the entry (which dominates all + basic blocks), but this is done for expedience. + Without this step, sccp fails, possibly because dominators are not + guaranteed to be traversed first. + """ + + def run_pass(self): + entry_bb = self.function.entry + assert entry_bb.is_terminated + tmp = entry_bb.instructions.pop() + + for bb in self.function.get_basic_blocks(): + if bb is entry_bb: + continue + + # Extract alloca instructions + non_alloca_instructions = [] + for inst in bb.instructions: + if inst.opcode in ("alloca", "palloca"): + # note: order of allocas impacts bytecode. + # TODO: investigate. + entry_bb.insert_instruction(inst) + else: + non_alloca_instructions.append(inst) + + # Replace original instructions with filtered list + bb.instructions = non_alloca_instructions + + entry_bb.instructions.append(tmp) diff --git a/vyper/venom/passes/sccp/sccp.py b/vyper/venom/passes/sccp/sccp.py index 2be84ce502..369be3e753 100644 --- a/vyper/venom/passes/sccp/sccp.py +++ b/vyper/venom/passes/sccp/sccp.py @@ -252,7 +252,7 @@ def finalize(ret): if eval_result is LatticeEnum.BOTTOM: return finalize(LatticeEnum.BOTTOM) - assert isinstance(eval_result, IROperand) + assert isinstance(eval_result, IROperand), (inst.parent.label, op, inst) ops.append(eval_result) # If we haven't found BOTTOM yet, evaluate the operation