diff --git a/tests/functional/builtins/codegen/test_create_functions.py b/tests/functional/builtins/codegen/test_create_functions.py index b7134d664d..085d012759 100644 --- a/tests/functional/builtins/codegen/test_create_functions.py +++ b/tests/functional/builtins/codegen/test_create_functions.py @@ -785,6 +785,30 @@ def deploy_from_memory() -> address: assert env.get_code(res) == runtime +# `initcode` and `value` arguments overlap +def test_raw_create_memory_overlap(get_contract, env): + to_deploy_code = """ +foo: public(uint256) + """ + + out = compile_code(to_deploy_code, output_formats=["bytecode", "bytecode_runtime"]) + initcode = bytes.fromhex(out["bytecode"].removeprefix("0x")) + runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x")) + + deployer_code = """ +@external +def deploy_from_calldata(s: Bytes[49152]) -> address: + v: DynArray[Bytes[49152], 2] = [s] + x: address = raw_create(v[0], value = 0 if v.pop() == b'' else 0, revert_on_failure=False) + return x + """ + + deployer = get_contract(deployer_code) + + res = deployer.deploy_from_calldata(initcode) + assert env.get_code(res) == runtime + + def test_raw_create_double_eval(get_contract, env): to_deploy_code = """ foo: public(uint256) @@ -1095,6 +1119,8 @@ def __init__(arg: uint256): initcode = bytes.fromhex(out["bytecode"].removeprefix("0x")) runtime = bytes.fromhex(out["bytecode_runtime"].removeprefix("0x")) + # the implementation of `raw_create` firstly caches + # `value` and then `salt`, here the source order is `salt` then `value` deployer_code = """ c: Bytes[1024] salt: bytes32 @@ -1122,3 +1148,33 @@ def deploy_from_calldata(s: Bytes[1024], arg: uint256, salt: bytes32, value_: ui assert HexBytes(res) == create2_address_of(deployer.address, salt, initcode) assert env.get_code(res) == runtime assert env.get_balance(res) == value + + +# test vararg and kwarg order of evaluation +# test fails because `value` gets evaluated +# before the 1st vararg which doesn't follow +# source code order +@pytest.mark.xfail(raises=AssertionError) +def test_raw_create_eval_order(get_contract): + code = """ +a: public(uint256) + +@deploy +def __init__(): + initcode: Bytes[100] = b"a" + res: address = raw_create( + initcode, self.test1(), value=self.test2(), revert_on_failure=False + ) + +@internal +def test1() -> uint256: + self.a = 1 + return 1 + +@internal +def test2() -> uint256: + self.a = 2 + return 2 + """ + c = get_contract(code) + assert c.a() == 2 diff --git a/tests/hevm.py b/tests/hevm.py index 5ab36b2ec6..da104db60a 100644 --- a/tests/hevm.py +++ b/tests/hevm.py @@ -5,7 +5,7 @@ from tests.venom_utils import parse_from_basic_block from vyper.ir.compile_ir import assembly_to_evm -from vyper.venom import LowerDloadPass, SimplifyCFGPass, StoreExpansionPass, VenomCompiler +from vyper.venom import LowerDloadPass, SimplifyCFGPass, SingleUseExpansion, VenomCompiler from vyper.venom.analysis import IRAnalysesCache from vyper.venom.basicblock import IRInstruction, IRLiteral @@ -64,7 +64,7 @@ def _prep_hevm_venom_ctx(ctx, verbose=False): # requirements for venom_to_assembly LowerDloadPass(ac, fn).run_pass() - StoreExpansionPass(ac, fn).run_pass() + SingleUseExpansion(ac, fn).run_pass() compiler = VenomCompiler([ctx]) asm = compiler.generate_evm(no_optimize=False) diff --git a/tests/unit/compiler/venom/test_algebraic_binopt.py b/tests/unit/compiler/venom/test_algebraic_binopt.py index 4c3b6dc247..b808ba6e8e 100644 --- a/tests/unit/compiler/venom/test_algebraic_binopt.py +++ b/tests/unit/compiler/venom/test_algebraic_binopt.py @@ -1,7 +1,7 @@ import pytest from tests.venom_utils import PrePostChecker -from vyper.venom.passes import AlgebraicOptimizationPass, StoreElimination +from vyper.venom.passes import AlgebraicOptimizationPass, AssignElimination """ Test abstract binop+unop optimizations in algebraic optimizations pass @@ -9,7 +9,7 @@ pytestmark = pytest.mark.hevm -_check_pre_post = PrePostChecker([StoreElimination, AlgebraicOptimizationPass, StoreElimination]) +_check_pre_post = PrePostChecker([AssignElimination, AlgebraicOptimizationPass, AssignElimination]) def test_sccp_algebraic_opt_sub_xor(): diff --git a/tests/unit/compiler/venom/test_duplicate_operands.py b/tests/unit/compiler/venom/test_duplicate_operands.py index 89b06796e3..b3b466f75a 100644 --- a/tests/unit/compiler/venom/test_duplicate_operands.py +++ b/tests/unit/compiler/venom/test_duplicate_operands.py @@ -2,7 +2,7 @@ from vyper.venom import generate_assembly_experimental from vyper.venom.analysis import IRAnalysesCache from vyper.venom.context import IRContext -from vyper.venom.passes import StoreExpansionPass +from vyper.venom.passes import SingleUseExpansion def test_duplicate_operands(): @@ -26,7 +26,7 @@ def test_duplicate_operands(): bb.append_instruction("stop") ac = IRAnalysesCache(fn) - StoreExpansionPass(ac, fn).run_pass() + SingleUseExpansion(ac, fn).run_pass() optimize = OptimizationLevel.GAS asm = generate_assembly_experimental(ctx, optimize=optimize) diff --git a/tests/unit/compiler/venom/test_load_elimination.py b/tests/unit/compiler/venom/test_load_elimination.py index 23182da21e..f8bb322bcf 100644 --- a/tests/unit/compiler/venom/test_load_elimination.py +++ b/tests/unit/compiler/venom/test_load_elimination.py @@ -2,7 +2,7 @@ from tests.venom_utils import PrePostChecker from vyper.evm.address_space import CALLDATA, DATA, MEMORY, STORAGE, TRANSIENT -from vyper.venom.passes import LoadElimination, StoreElimination +from vyper.venom.passes import AssignElimination, LoadElimination pytestmark = pytest.mark.hevm @@ -11,7 +11,7 @@ # and the second/in post is needed to create # easier equivalence in the test for pre and post _check_pre_post = PrePostChecker( - passes=[StoreElimination, LoadElimination, StoreElimination], post=[StoreElimination] + passes=[AssignElimination, LoadElimination, AssignElimination], post=[AssignElimination] ) diff --git a/tests/unit/compiler/venom/test_phi_elimination.py b/tests/unit/compiler/venom/test_phi_elimination.py new file mode 100644 index 0000000000..568477db5b --- /dev/null +++ b/tests/unit/compiler/venom/test_phi_elimination.py @@ -0,0 +1,321 @@ +import pytest + +from tests.venom_utils import PrePostChecker +from vyper.venom.passes import PhiEliminationPass + +_check_pre_post = PrePostChecker([PhiEliminationPass], default_hevm=False) + + +@pytest.mark.hevm +def test_simple_phi_elimination(): + pre = """ + main: + %1 = param + %cond = param + %2 = %1 + jnz %cond, @then, @else + then: + jmp @else + else: + %3 = phi @main, %1, @else, %2 + sink %3 + """ + + post = """ + main: + %1 = param + %cond = param + %2 = %1 + jnz %cond, @then, @else + then: + jmp @else + else: + %3 = %1 + sink %3 + """ + + _check_pre_post(pre, post, hevm=True) + + +def test_phi_elim_loop(): + pre = """ + main: + %v = param + jmp @loop + loop: + %v:2 = phi @main, %v, @loop, %v:2 + jmp @loop + """ + + post = """ + main: + %v = param + jmp @loop + loop: + %v:2 = %v + jmp @loop + """ + + _check_pre_post(pre, post) + + +def test_phi_elim_loop2(): + pre = """ + main: + %1 = calldataload 0 + %2 = %1 + jmp @condition + + condition: + %3 = phi @main, %1, @body, %4 + %cond = calldataload 100 + jnz %cond, @exit, @body + + body: + %4 = %2 + %another_cond = calldataload 200 + jnz %another_cond, @condition, @exit + + exit: + %5 = phi @condition, %3, @body, %4 + mstore 0, %5 + return 0, 32 + """ + + post = """ + main: + %1 = calldataload 0 + %2 = %1 + jmp @condition + + condition: + %3 = %1 + %cond = calldataload 100 + jnz %cond, @exit, @body + + body: + %4 = %2 + %another_cond = calldataload 200 + jnz %another_cond, @condition, @exit + + exit: + %5 = %1 + mstore 0, %5 + return 0, 32 + """ + + _check_pre_post(pre, post) + + +def test_phi_elim_loop_inner_phi(): + pre = """ + main: + %1 = param + %rand = param + %2 = %1 + jmp @condition + condition: + %3 = phi @main, %1, @body, %6 + %cond = iszero %3 + jnz %cond, @exit, @body + body: + %4 = %2 + %another_cond = calldataload 200 + jnz %rand, @then, @else + then: + %6:1 = %4 + jmp @join + else: + %6:2 = %1 + jmp @join + join: + %6 = phi @then, %6:1, @else, %6:2 + jnz %another_cond, @condition, @exit + exit: + %5 = phi @condition, %3, @body, %4 + sink %5 + """ + + post = """ + main: + %1 = param + %rand = param + %2 = %1 + jmp @condition + condition: + %3 = %1 + %cond = iszero %3 + jnz %cond, @exit, @body + body: + %4 = %2 + %another_cond = calldataload 200 + jnz %rand, @then, @else + then: + %6:1 = %4 + jmp @join + else: + %6:2 = %1 + jmp @join + join: + %6 = %1 + jnz %another_cond, @condition, @exit + exit: + %5 = %1 + sink %5 + """ + + _check_pre_post(pre, post) + + +def test_phi_elim_loop_inner_phi_simple(): + pre = """ + main: + %p = param + jmp @loop_start + loop_start: + %1 = phi @main, %p, @loop_join, %4 + jnz %1, @then, @else + then: + %2 = %1 + jmp @loop_join + else: + %3 = %1 + jmp @loop_join + loop_join: + %4 = phi @then, %2, @else, %3 + jmp @loop_start + """ + + post = """ + main: + %p = param + jmp @loop_start + loop_start: + %1 = %p + jnz %1, @then, @else + then: + %2 = %1 + jmp @loop_join + else: + %3 = %1 + jmp @loop_join + loop_join: + %4 = %p + jmp @loop_start + """ + + _check_pre_post(pre, post) + + +def test_phi_elim_cannot_remove(): + pre = """ + main: + %p = param + %rand = param + jmp @cond + cond: + %1 = phi @main, %p, @body, %3 + %cond = iszero %1 + jnz %cond, @body, @join + body: + jnz %rand, @then, @join + then: + %2 = 2 + jmp @join + join: + %3 = phi @body, %1, @then, %2 + jmp @cond + exit: + sink %p + """ + + _check_pre_post(pre, pre, hevm=False) + + +def test_phi_elim_direct_loop(): + pre1 = """ + main: + %p = param + jmp @loop + loop: + %1 = phi @main, %p, @loop, %2 + %2 = %1 + jmp @loop + """ + + pre2 = """ + main: + %p = param + jmp @loop + loop: + %1 = phi @main, %p, @loop, %2 + %2 = %1 + jmp @loop + """ + + post = """ + main: + %p = param + jmp @loop + loop: + %1 = %p + %2 = %1 + jmp @loop + """ + + _check_pre_post(pre1, post) + _check_pre_post(pre2, post) + + +def test_phi_elim_two_phi_merges(): + pre = """ + main: + %cond = param + %cond2 = param + jnz %cond, @1_then, @2_then + 1_then: + %1 = 100 + jmp @3_join + 2_then: + %2 = 101 + jmp @3_join + 3_join: + %3 = phi @1_then, %1, @2_then, %2 + jnz %cond2, @4_then, @5_then + 4_then: + %4 = %3 + jmp @6_join + 5_then: + %5 = %3 + jmp @6_join + 6_join: + %6 = phi @4_then, %4, @5_then, %5 ; should be reduced to %3! + sink %6 + """ + + post = """ + main: + %cond = param + %cond2 = param + jnz %cond, @1_then, @2_then + 1_then: + %1 = 100 + jmp @3_join + 2_then: + %2 = 101 + jmp @3_join + 3_join: + %3 = phi @1_then, %1, @2_then, %2 + jnz %cond2, @4_then, @5_then + 4_then: + %4 = %3 + jmp @6_join + 5_then: + %5 = %3 + jmp @6_join + 6_join: + %6 = %3 + sink %6 + """ + + _check_pre_post(pre, post, hevm=True) diff --git a/tests/unit/compiler/venom/test_store_expansion.py b/tests/unit/compiler/venom/test_single_use_expansion.py similarity index 81% rename from tests/unit/compiler/venom/test_store_expansion.py rename to tests/unit/compiler/venom/test_single_use_expansion.py index 8d5fd2fc9c..d1c6414c86 100644 --- a/tests/unit/compiler/venom/test_store_expansion.py +++ b/tests/unit/compiler/venom/test_single_use_expansion.py @@ -3,17 +3,17 @@ from tests.venom_utils import parse_from_basic_block from vyper.venom import generate_assembly_experimental from vyper.venom.analysis import IRAnalysesCache -from vyper.venom.passes import StoreExpansionPass +from vyper.venom.passes import SingleUseExpansion -def test_store_expansion(): +def test_single_use_Expansion(): """ Test to was created from the example in the issue https://github.com/vyperlang/vyper/issues/4215 - it issue is handled by StoreExpansionPass + it issue is handled by the SingleUseExpansion pass Originally it was handled by different reorder algorithm - which is not necessary with store expansion + which is not necessary with single-use expansion """ code = """ @@ -43,6 +43,6 @@ def test_store_expansion(): for fn in ctx.functions.values(): ac = IRAnalysesCache(fn) - StoreExpansionPass(ac, fn).run_pass() + SingleUseExpansion(ac, fn).run_pass() generate_assembly_experimental(ctx) diff --git a/vyper/builtins/functions.py b/vyper/builtins/functions.py index eab04a2e4b..0da0b5571f 100644 --- a/vyper/builtins/functions.py +++ b/vyper/builtins/functions.py @@ -191,37 +191,105 @@ def build_IR(self, expr, args, kwargs, context): class Convert(BuiltinFunctionT): _id = "convert" - def fetch_call_return(self, node): - _, target_typedef = self.infer_arg_types(node) - - # note: more type conversion validation happens in convert.py - return target_typedef.typedef - - # TODO: push this down into convert.py for more consistency - def infer_arg_types(self, node, expected_return_typ=None): + def _try_fold(self, node): validate_call_args(node, 2) + value = node.args[0].get_folded_value() + target_type = node.args[1].get_folded_value() + if not isinstance(target_type, vy_ast.Name): + raise UnfoldableNode - target_type = type_from_annotation(node.args[1]) - value_types = get_possible_types_from_node(node.args[0]) - - # For `convert` of integer literals, we need to match type inference rules in - # convert.py codegen routines. - # TODO: This can probably be removed once constant folding for `convert` is implemented - if len(value_types) > 1 and all(isinstance(v, IntegerT) for v in value_types): - # Get the smallest (and unsigned if available) type for non-integer target types - # (note this is different from the ordering returned by `get_possible_types_from_node`) - if not isinstance(target_type, IntegerT): - value_types = sorted(value_types, key=lambda v: (v.is_signed, v.bits), reverse=True) + target_typedef = type_from_annotation(target_type) + if not isinstance(target_typedef, (BoolT, AddressT, IntegerT, BytesM_T)): + raise UnfoldableNode + + if isinstance(target_typedef, BoolT): + if isinstance(value, vy_ast.Int): + result = bool(value.value) + elif isinstance(value, vy_ast.Bool): + result = value.value + else: + raise UnfoldableNode + return vy_ast.Bool.from_node(node, value=result) + + elif isinstance(target_typedef, AddressT): + if isinstance(value, vy_ast.Hex): + if not value.value.startswith("0x"): + raise InvalidLiteral("Address must start with 0x", node.args[0]) + try: + addr_bytes = value.bytes_value + if len(addr_bytes) != 20: + raise InvalidLiteral("Address must be exactly 20 bytes", node.args[0]) + result = value.value + except ValueError: + raise InvalidLiteral("Invalid hex literal for address", node.args[0]) + elif isinstance(value, vy_ast.Int): + # Convert integer to address (right-padded with zeros) + addr_bytes = value.value.to_bytes(32, "big")[-20:] + result = f"0x{addr_bytes.hex()}" + else: + raise UnfoldableNode + return vy_ast.Hex.from_node(node, value=result) + + elif isinstance(target_typedef, IntegerT): + if isinstance(value, vy_ast.Decimal): + if value.value % 1 != 0: + raise InvalidLiteral("Cannot truncate decimal when converting to integer", node.args[0]) + result = int(value.value) + elif isinstance(value, vy_ast.Int): + result = value.value else: - # filter out the target type from list of possible types - value_types = [i for i in value_types if not target_type.compare_type(i)] + raise UnfoldableNode + lo, hi = target_typedef.ast_bounds + if result < lo or result > hi: + raise InvalidLiteral( + f"value {result} out of range for {target_typedef}", node.args[0] + ) + return vy_ast.Int.from_node(node, value=result) + + elif isinstance(target_typedef, BytesM_T): + if isinstance(value, vy_ast.Hex): + if not value.value.startswith("0x"): + raise InvalidLiteral("Bytes must start with 0x", node.args[0]) + try: + bytes_value = value.bytes_value + if len(bytes_value) > target_typedef.length: + raise InvalidLiteral( + f"Bytes literal too long for {target_typedef}", node.args[0] + ) + # Left-pad with zeros if shorter than target length + if len(bytes_value) < target_typedef.length: + bytes_value = bytes_value.rjust(target_typedef.length, b"\x00") + result = f"0x{bytes_value.hex()}" + except ValueError: + raise InvalidLiteral("Invalid hex literal for bytes", node.args[0]) + elif isinstance(value, vy_ast.Bytes): + bytes_value = value.value + if len(bytes_value) > target_typedef.length: + raise InvalidLiteral( + f"Bytes literal too long for {target_typedef}", node.args[0] + ) + # Left-pad with zeros if shorter than target length + if len(bytes_value) < target_typedef.length: + bytes_value = bytes_value.rjust(target_typedef.length, b"\x00") + result = f"0x{bytes_value.hex()}" + else: + raise UnfoldableNode + return vy_ast.Hex.from_node(node, value=result) - value_type = value_types.pop() + raise UnfoldableNode - # block conversions between same type - if target_type.compare_type(value_type): - raise InvalidType(f"Value and target type are both '{target_type}'", node) + def fetch_call_return(self, node): + _, target_typedef = self.infer_arg_types(node) + return target_typedef.typedef + def infer_arg_types(self, node, expected_return_typ=None): + self._validate_arg_types(node) + possible = sorted( + get_possible_types_from_node(node.args[0]), + key=lambda t: (str(t.typ), getattr(t, "bits", 0)) + ) + value_type = possible[0] + target_type = type_from_annotation(node.args[1]) return [value_type, TYPE_T(target_type)] def build_IR(self, expr, context): @@ -290,6 +358,50 @@ class Slice(BuiltinFunctionT): ("length", UINT256_T), ] + def _try_fold(self, node): + validate_call_args(node, 3) + value = node.args[0].get_folded_value() + start = node.args[1].get_folded_value() + length = node.args[2].get_folded_value() + + if not isinstance(start, vy_ast.Int) or not isinstance(length, vy_ast.Int): + raise UnfoldableNode + + start_val = start.value + length_val = length.value + + if length_val < 1: + raise ArgumentException("Length cannot be less than 1", node.args[2]) + if start_val < 0: + raise ArgumentException("Start index cannot be negative", node.args[1]) + + if isinstance(value, vy_ast.Bytes): + if start_val + length_val > len(value.value): + raise ArgumentException( + f"slice out of bounds: start={start_val}, length={length_val}, max_length={len(value.value)}", + node + ) + result = value.value[start_val:start_val + length_val] + return vy_ast.Bytes.from_node(node, value=result) + elif isinstance(value, vy_ast.Str): + if start_val + length_val > len(value.value): + raise ArgumentException( + f"slice out of bounds: start={start_val}, length={length_val}, max_length={len(value.value)}", + node + ) + result = value.value[start_val:start_val + length_val] + return vy_ast.Str.from_node(node, value=result) + elif isinstance(value, vy_ast.Hex): + if start_val + length_val > len(value.bytes_value): + raise ArgumentException( + f"slice out of bounds: start={start_val}, length={length_val}, max_length={len(value.bytes_value)}", + node + ) + result = value.bytes_value[start_val:start_val + length_val] + return vy_ast.Bytes.from_node(node, value=result) + else: + raise UnfoldableNode + def fetch_call_return(self, node): arg_type, _, _ = self.infer_arg_types(node) @@ -336,8 +448,11 @@ def fetch_call_return(self, node): def infer_arg_types(self, node, expected_return_typ=None): self._validate_arg_types(node) - # return a concrete type for `b` - b_type = get_possible_types_from_node(node.args[0]).pop() + possible = sorted( + get_possible_types_from_node(node.args[0]), + key=lambda t: (str(t.typ), getattr(t, "bits", 0)) + ) + b_type = possible[0] return [b_type, self._inputs[1][1], self._inputs[2][1]] @process_inputs @@ -833,6 +948,51 @@ class Extract32(BuiltinFunctionT): _inputs = [("b", BytesT.any()), ("start", IntegerT.unsigneds())] _kwargs = {"output_type": KwargSettings(TYPE_T.any(), BYTES32_T)} + def _try_fold(self, node): + validate_call_args(node, 2) + value = node.args[0].get_folded_value() + start = node.args[1].get_folded_value() + + if not isinstance(start, vy_ast.Int): + raise UnfoldableNode + + start_val = start.value + + if start_val < 0: + raise ArgumentException("Start index cannot be negative", node.args[1]) + + output_type = self.infer_kwarg_types(node)["output_type"].typedef + + if isinstance(value, vy_ast.Bytes): + if start_val + 32 > len(value.value): + raise ArgumentException( + f"extract32 out of bounds: start={start_val}, max_length={len(value.value)}", + node + ) + result = value.value[start_val:start_val + 32] + elif isinstance(value, vy_ast.Hex): + if start_val + 32 > len(value.bytes_value): + raise ArgumentException( + f"extract32 out of bounds: start={start_val}, max_length={len(value.bytes_value)}", + node + ) + result = value.bytes_value[start_val:start_val + 32] + else: + raise UnfoldableNode + + if isinstance(output_type, BytesM_T): + expected = output_type.length + if expected != 32: + result = result[:expected] + return vy_ast.Hex.from_node(node, value=f"0x{result.hex()}") + elif isinstance(output_type, IntegerT): + return vy_ast.Int.from_node(node, value=int.from_bytes(result, "big")) + elif isinstance(output_type, AddressT): + # right-align as per ABI: take the last 20 bytes + return vy_ast.Hex.from_node(node, value=f"0x{result[-20:].hex()}") + else: + raise UnfoldableNode + def fetch_call_return(self, node): self._validate_arg_types(node) return_type = self.infer_kwarg_types(node)["output_type"].typedef @@ -840,7 +1000,11 @@ def fetch_call_return(self, node): def infer_arg_types(self, node, expected_return_typ=None): self._validate_arg_types(node) - input_type = get_possible_types_from_node(node.args[0]).pop() + possible = sorted( + get_possible_types_from_node(node.args[0]), + key=lambda t: (str(t.typ), getattr(t, "bits", 0)) + ) + input_type = possible[0] return [input_type, UINT256_T] def infer_kwarg_types(self, node): @@ -1602,6 +1766,14 @@ def _build_create_IR(self, expr, args, context, value, salt, revert_on_failure): initcode = args[0] ctor_args = args[1:] + if any(potential_overlap(initcode, other) for other in ctor_args + [value, salt]): + # value or salt could be expressions which trample the initcode + # buffer. cf. test_raw_create_memory_overlap + # note that potential_overlap is overly conservative, since it + # checks for the existence of calls (which are not applicable + # here, since `initcode` is guaranteed to be in memory). + initcode = create_memory_copy(initcode, context) + # encode the varargs to_encode = ir_tuple_from_args(ctor_args) type_size_bound = to_encode.typ.abi_type.size_bound() diff --git a/vyper/codegen/core.py b/vyper/codegen/core.py index ea51eda832..e885f4c6a4 100644 --- a/vyper/codegen/core.py +++ b/vyper/codegen/core.py @@ -18,7 +18,6 @@ AddressT, BoolT, BytesM_T, - BytesT, DArrayT, DecimalT, HashMapT, @@ -26,6 +25,7 @@ InterfaceT, StructT, TupleT, + VyperType, _BytestringT, ) from vyper.semantics.types.shortcuts import BYTES32_T, INT256_T, UINT256_T @@ -71,6 +71,26 @@ def is_array_like(typ): return ret +class _InternalBufferT(VyperType): + _invalid_locations = tuple(DataLocation) + + def __init__(self, buf_size: int): + assert buf_size >= 0 + self.buf_size: int = ceil32(buf_size) + + super().__init__(members=None) + + @property + def size_in_bytes(self): + return self.buf_size + + def get_size_in(self, location: DataLocation) -> int: # pragma: nocover + # get_size_in should only be called by semantic analysis. by the + # time we get to codegen, this should never be called. (if this + # assumption changes in the future, we can lift the restriction). + raise CompilerPanic("internal buffer should only be used in memory!") + + def get_type_for_exact_size(n_bytes): """Create a type which will take up exactly n_bytes. Used for allocating internal buffers. @@ -79,7 +99,7 @@ def get_type_for_exact_size(n_bytes): Returns: type: A type which can be passed to context.new_variable """ - return BytesT(n_bytes - 32 * DYNAMIC_ARRAY_OVERHEAD) + return _InternalBufferT(n_bytes) # propagate revert message when calls to external contracts fail diff --git a/vyper/codegen/memory_allocator.py b/vyper/codegen/memory_allocator.py index f31148825c..945352db2e 100644 --- a/vyper/codegen/memory_allocator.py +++ b/vyper/codegen/memory_allocator.py @@ -28,7 +28,7 @@ def partially_allocate(self, size: int) -> int: int Position of the newly allocated memory """ - if size >= self.size: + if size >= self.size: # pragma: nocover raise CompilerPanic("Attempted to allocate more memory than available") position = self.position self.position += size @@ -88,8 +88,11 @@ def allocate_memory(self, size: int) -> int: int Start offset of the newly allocated memory. """ - if size % 32 != 0: + if size % 32 != 0: # pragma: nocover raise CompilerPanic(f"tried to allocate {size} bytes, only multiples of 32 supported.") + if size < 0: # pragma: nocover + # sanity check + raise CompilerPanic(f"tried to allocate {size} bytes") # check for deallocated memory prior to expanding for i, free_memory in enumerate(self.deallocated_mem): diff --git a/vyper/semantics/types/base.py b/vyper/semantics/types/base.py index 84624965db..d51cfadd9a 100644 --- a/vyper/semantics/types/base.py +++ b/vyper/semantics/types/base.py @@ -171,7 +171,7 @@ def abi_type(self) -> ABIType: """ raise CompilerPanic("Method must be implemented by the inherited class") - def get_size_in(self, location: DataLocation): + def get_size_in(self, location: DataLocation) -> int: if location in (DataLocation.STORAGE, DataLocation.TRANSIENT): return self.storage_size_in_words if location == DataLocation.MEMORY: diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index a7c9163597..25773114f3 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -14,6 +14,7 @@ CSE, SCCP, AlgebraicOptimizationPass, + AssignElimination, BranchOptimizationPass, DFTPass, FloatAllocas, @@ -23,12 +24,12 @@ MakeSSA, Mem2Var, MemMergePass, + PhiEliminationPass, ReduceLiteralsCodesize, RemoveUnusedVariablesPass, RevertToAssert, SimplifyCFGPass, - StoreElimination, - StoreExpansionPass, + SingleUseExpansion, ) from vyper.venom.venom_to_assembly import VenomCompiler @@ -59,19 +60,21 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel, ac: IRAnalysesCache SimplifyCFGPass(ac, fn).run_pass() MakeSSA(ac, fn).run_pass() + PhiEliminationPass(ac, fn).run_pass() # run algebraic opts before mem2var to reduce some pointer arithmetic AlgebraicOptimizationPass(ac, fn).run_pass() - StoreElimination(ac, fn).run_pass() + AssignElimination(ac, fn).run_pass() Mem2Var(ac, fn).run_pass() MakeSSA(ac, fn).run_pass() + PhiEliminationPass(ac, fn).run_pass() SCCP(ac, fn).run_pass() SimplifyCFGPass(ac, fn).run_pass() - StoreElimination(ac, fn).run_pass() + AssignElimination(ac, fn).run_pass() AlgebraicOptimizationPass(ac, fn).run_pass() LoadElimination(ac, fn).run_pass() SCCP(ac, fn).run_pass() - StoreElimination(ac, fn).run_pass() + AssignElimination(ac, fn).run_pass() RevertToAssert(ac, fn).run_pass() SimplifyCFGPass(ac, fn).run_pass() @@ -85,11 +88,12 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel, ac: IRAnalysesCache # This improves the performance of cse RemoveUnusedVariablesPass(ac, fn).run_pass() - StoreElimination(ac, fn).run_pass() + PhiEliminationPass(ac, fn).run_pass() + AssignElimination(ac, fn).run_pass() CSE(ac, fn).run_pass() - StoreElimination(ac, fn).run_pass() + AssignElimination(ac, fn).run_pass() RemoveUnusedVariablesPass(ac, fn).run_pass() - StoreExpansionPass(ac, fn).run_pass() + SingleUseExpansion(ac, fn).run_pass() if optimize == OptimizationLevel.CODESIZE: ReduceLiteralsCodesize(ac, fn).run_pass() diff --git a/vyper/venom/passes/__init__.py b/vyper/venom/passes/__init__.py index 429f1ec8e3..ada15a4d1b 100644 --- a/vyper/venom/passes/__init__.py +++ b/vyper/venom/passes/__init__.py @@ -1,4 +1,5 @@ from .algebraic_optimization import AlgebraicOptimizationPass +from .assign_elimination import AssignElimination from .branch_optimization import BranchOptimizationPass from .common_subexpression_elimination import CSE from .dft import DFTPass @@ -11,9 +12,9 @@ from .mem2var import Mem2Var from .memmerging import MemMergePass from .normalization import NormalizationPass +from .phi_elimination import PhiEliminationPass from .remove_unused_variables import RemoveUnusedVariablesPass from .revert_to_assert import RevertToAssert from .sccp import SCCP from .simplify_cfg import SimplifyCFGPass -from .store_elimination import StoreElimination -from .store_expansion import StoreExpansionPass +from .single_use_expansion import SingleUseExpansion diff --git a/vyper/venom/passes/store_elimination.py b/vyper/venom/passes/assign_elimination.py similarity index 89% rename from vyper/venom/passes/store_elimination.py rename to vyper/venom/passes/assign_elimination.py index f787bc8ed5..d98688f3ef 100644 --- a/vyper/venom/passes/store_elimination.py +++ b/vyper/venom/passes/assign_elimination.py @@ -3,10 +3,11 @@ from vyper.venom.passes.base_pass import InstUpdater, IRPass -class StoreElimination(IRPass): +class AssignElimination(IRPass): """ This pass forwards variables to their uses though `store` instructions, - and removes the `store` instruction. + and removes the `store` instruction. In the future we will probably rename + the `store` instruction to `"assign"`. """ # TODO: consider renaming `store` instruction, since it is confusing diff --git a/vyper/venom/passes/machinery/inst_updater.py b/vyper/venom/passes/machinery/inst_updater.py index be3581ec7e..dca98841d9 100644 --- a/vyper/venom/passes/machinery/inst_updater.py +++ b/vyper/venom/passes/machinery/inst_updater.py @@ -35,7 +35,7 @@ def update( new_operands: list[IROperand], new_output: Optional[IRVariable] = None, ): - assert opcode != "phi" + # assert opcode != "phi" # sanity assert all(isinstance(op, IROperand) for op in new_operands) diff --git a/vyper/venom/passes/phi_elimination.py b/vyper/venom/passes/phi_elimination.py new file mode 100644 index 0000000000..d9f9d26ab3 --- /dev/null +++ b/vyper/venom/passes/phi_elimination.py @@ -0,0 +1,97 @@ +from vyper.venom.analysis import DFGAnalysis, LivenessAnalysis +from vyper.venom.basicblock import IRInstruction, IRVariable +from vyper.venom.passes.base_pass import InstUpdater, IRPass + + +class PhiEliminationPass(IRPass): + phi_to_origins: dict[IRInstruction, set[IRInstruction]] + + def run_pass(self): + self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) + self.updater = InstUpdater(self.dfg) + self._calculate_phi_origins() + + for _, inst in self.dfg.outputs.copy().items(): + if inst.opcode != "phi": + continue + self._process_phi(inst) + + # sort phis to top of basic block + for bb in self.function.get_basic_blocks(): + bb.ensure_well_formed() + + self.analyses_cache.invalidate_analysis(LivenessAnalysis) + + def _process_phi(self, inst: IRInstruction): + srcs = self.phi_to_origins[inst] + + if len(srcs) == 1: + src = srcs.pop() + if src == inst: + return + assert src.output is not None + self.updater.store(inst, src.output) + + def _calculate_phi_origins(self): + self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) + self.phi_to_origins = dict() + + for bb in self.function.get_basic_blocks(): + for inst in bb.instructions: + if inst.opcode != "phi": + break + self._get_phi_origins(inst) + + def _get_phi_origins(self, inst: IRInstruction): + assert inst.opcode == "phi" # sanity + visited: set[IRInstruction] = set() + self.phi_to_origins[inst] = self._get_phi_origins_r(inst, visited) + + # traverse chains of phis and stores to get the "root" instructions + # for phis. + def _get_phi_origins_r( + self, inst: IRInstruction, visited: set[IRInstruction] + ) -> set[IRInstruction]: + if inst.opcode == "phi": + if inst in self.phi_to_origins: + return self.phi_to_origins[inst] + + if inst in visited: + # we have hit a dfg cycle. break the recursion. + # if it is only visited we have found a self + # reference, and we won't find anything more by + # continuing the recursion. + return set() + + visited.add(inst) + + res: set[IRInstruction] = set() + + for _, var in inst.phi_operands: + next_inst = self.dfg.get_producing_instruction(var) + assert next_inst is not None, (inst, var) + res |= self._get_phi_origins_r(next_inst, visited) + + if len(res) > 1: + # if this phi has more than one origin, then for future + # phis, it is better to treat this as a barrier in the + # graph traversal. for example (without basic blocks) + # %a = 1 + # %b = 2 + # %c = phi %a, %b ; has two origins + # %d = %c + # %e = %d + # %f = phi %d, %e + # in this case, %f should reduce to %c. + return set([inst]) + return res + + if inst.opcode == "store" and isinstance(inst.operands[0], IRVariable): + # traverse store chain + var = inst.operands[0] + next_inst = self.dfg.get_producing_instruction(var) + assert next_inst is not None + return self._get_phi_origins_r(next_inst, visited) + + # root of the phi/store chain + return set([inst]) diff --git a/vyper/venom/passes/store_expansion.py b/vyper/venom/passes/single_use_expansion.py similarity index 66% rename from vyper/venom/passes/store_expansion.py rename to vyper/venom/passes/single_use_expansion.py index be5eb3d95d..50dc16aea6 100644 --- a/vyper/venom/passes/store_expansion.py +++ b/vyper/venom/passes/single_use_expansion.py @@ -3,10 +3,20 @@ from vyper.venom.passes.base_pass import IRPass -class StoreExpansionPass(IRPass): +class SingleUseExpansion(IRPass): """ - This pass extracts literals and variables so that they can be - reordered by the DFT pass + This pass transforms venom IR to "single use" form. It extracts literals + and variables so that they can be reordered by the DFT pass. It creates + two invariants: + - each variable is used at most once (by any opcode besides a simple + assignment) + - operands to all instructions (besides assignment instructions) must + be variables. + + these two properties are helpful for DFT and venom_to_assembly.py, and + in fact the first invariant is *required* by venom_to_assembly.py. + + This pass is in some sense the "inverse" of AssignElimination. """ def run_pass(self):