From c9d8f5b8e8548b8af88581401a73fb1388040b14 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 25 Dec 2024 01:50:40 +0200 Subject: [PATCH 1/6] fix[venom]: fix `MakeSSA` with existing phis (#4423) This commit improves the `MakeSSA` pass to handle incoming `phi` instructions. Following the implementation of the venom parser it is possible to parse code with existing `phi` instructions in the code. The old code was expecting the `phi` instructions to have been self-placed and in "degenerate form" of output == all branch arguments. The new code does not make this assumption. This commit additionally: - refactors the existing `make_ssa` tests to use new test machinery - adds a phi parsing test (does not test the new code in this commit since it does not run passes, but does make sure we can at least parse phis) - expands the venom round-trip tests to check that we can both a) run venom passes on parsed venom, and b) bytecode generation from round-tripping venom produced by the vyper frontend is equivalent to bytecode generation from the regular pipeline (directly from vyper source code) --------- Co-authored-by: Charles Cooper --- tests/functional/venom/parser/test_parsing.py | 121 +++++++++++++++++- tests/functional/venom/test_venom_repr.py | 86 ++++++++++++- tests/unit/compiler/venom/test_make_ssa.py | 88 +++++++------ vyper/venom/basicblock.py | 17 ++- vyper/venom/passes/make_ssa.py | 8 +- 5 files changed, 263 insertions(+), 57 deletions(-) diff --git a/tests/functional/venom/parser/test_parsing.py b/tests/functional/venom/parser/test_parsing.py index f18a51fe76..bd536a8cfa 100644 --- a/tests/functional/venom/parser/test_parsing.py +++ b/tests/functional/venom/parser/test_parsing.py @@ -1,4 +1,4 @@ -from tests.venom_utils import assert_ctx_eq +from tests.venom_utils import assert_bb_eq, assert_ctx_eq from vyper.venom.basicblock import IRBasicBlock, IRLabel, IRLiteral, IRVariable from vyper.venom.context import DataItem, DataSection, IRContext from vyper.venom.function import IRFunction @@ -231,3 +231,122 @@ def test_multi_function_and_data(): ] assert_ctx_eq(parsed_ctx, expected_ctx) + + +def test_phis(): + # @external + # def _loop() -> uint256: + # res: uint256 = 9 + # for i: uint256 in range(res, bound=10): + # res = res + i + # return res + source = """ + function __main_entry { + __main_entry: ; IN=[] OUT=[fallback, 1_then] => {} + %27 = 0 + %1 = calldataload %27 + %28 = %1 + %29 = 224 + %2 = shr %29, %28 + %31 = %2 + %30 = 1729138561 + %4 = xor %30, %31 + %32 = %4 + jnz %32, @fallback, @1_then + ; (__main_entry) + + + 1_then: ; IN=[__main_entry] OUT=[4_condition] => {%11, %var8_0} + %6 = callvalue + %33 = %6 + %7 = iszero %33 + %34 = %7 + assert %34 + %var8_0 = 9 + %11 = 0 + nop + jmp @4_condition + ; (__main_entry) + + + 4_condition: ; IN=[1_then, 5_body] OUT=[5_body, 7_exit] => {%11:3, %var8_0:2} + %var8_0:2 = phi @1_then, %var8_0, @5_body, %var8_0:3 + %11:3 = phi @1_then, %11, @5_body, %11:4 + %35 = %11:3 + %36 = 9 + %15 = xor %36, %35 + %37 = %15 + jnz %37, @5_body, @7_exit + ; (__main_entry) + + + 5_body: ; IN=[4_condition] OUT=[4_condition] => {%11:4, %var8_0:3} + %38 = %11:3 + %39 = %var8_0:2 + %22 = add %39, %38 + %41 = %22 + %40 = %var8_0:2 + %24 = gt %40, %41 + %42 = %24 + %25 = iszero %42 + %43 = %25 + assert %43 + %var8_0:3 = %22 + %44 = %11:3 + %45 = 1 + %11:4 = add %45, %44 + jmp @4_condition + ; (__main_entry) + + + 7_exit: ; IN=[4_condition] OUT=[] => {} + %46 = %var8_0:2 + %47 = 64 + mstore %47, %46 + %48 = 32 + %49 = 64 + return %49, %48 + ; (__main_entry) + + + fallback: ; IN=[__main_entry] OUT=[] => {} + %50 = 0 + %51 = 0 + revert %51, %50 + stop + ; (__main_entry) + } ; close function __main_entry + """ + ctx = parse_venom(source) + + expected_ctx = IRContext() + expected_ctx.add_function(entry_fn := IRFunction(IRLabel("__main_entry"))) + + expect_bb = IRBasicBlock(IRLabel("4_condition"), entry_fn) + entry_fn.append_basic_block(expect_bb) + + expect_bb.append_instruction( + "phi", + IRLabel("1_then"), + IRVariable("%var8_0"), + IRLabel("5_body"), + IRVariable("%var8_0:3"), + ret=IRVariable("var8_0:2"), + ) + expect_bb.append_instruction( + "phi", + IRLabel("1_then"), + IRVariable("%11"), + IRLabel("5_body"), + IRVariable("%11:4"), + ret=IRVariable("11:3"), + ) + expect_bb.append_instruction("store", IRVariable("11:3"), ret=IRVariable("%35")) + expect_bb.append_instruction("store", IRLiteral(9), ret=IRVariable("%36")) + expect_bb.append_instruction("xor", IRVariable("%35"), IRVariable("%36"), ret=IRVariable("%15")) + expect_bb.append_instruction("store", IRVariable("%15"), ret=IRVariable("%37")) + expect_bb.append_instruction("jnz", IRVariable("%37"), IRLabel("5_body"), IRLabel("7_exit")) + # other basic blocks omitted for brevity + + parsed_fn = next(iter(ctx.functions.values())) + assert_bb_eq(parsed_fn.get_basic_block(expect_bb.label.name), expect_bb) diff --git a/tests/functional/venom/test_venom_repr.py b/tests/functional/venom/test_venom_repr.py index c25ce381d8..5136672a03 100644 --- a/tests/functional/venom/test_venom_repr.py +++ b/tests/functional/venom/test_venom_repr.py @@ -1,9 +1,13 @@ +import copy import glob +import textwrap import pytest from tests.venom_utils import assert_ctx_eq, parse_venom from vyper.compiler import compile_code +from vyper.compiler.phases import generate_bytecode +from vyper.venom import generate_assembly_experimental, run_passes_on from vyper.venom.context import IRContext """ @@ -16,15 +20,95 @@ def get_example_vy_filenames(): @pytest.mark.parametrize("vy_filename", get_example_vy_filenames()) -def test_round_trip(vy_filename, optimize, request): +def test_round_trip_examples(vy_filename, optimize, compiler_settings): + """ + Check all examples round trip + """ path = f"examples/{vy_filename}" with open(path) as f: vyper_source = f.read() + _round_trip_helper(vyper_source, optimize, compiler_settings) + + +# pure vyper sources +vyper_sources = [ + """ + @external + def _loop() -> uint256: + res: uint256 = 9 + for i: uint256 in range(res, bound=10): + res = res + i + return res + """ +] + + +@pytest.mark.parametrize("vyper_source", vyper_sources) +def test_round_trip_sources(vyper_source, optimize, compiler_settings): + """ + Test vyper_sources round trip + """ + vyper_source = textwrap.dedent(vyper_source) + _round_trip_helper(vyper_source, optimize, compiler_settings) + + +def _round_trip_helper(vyper_source, optimize, compiler_settings): + # helper function to test venom round-tripping thru the parser + # use two helpers because run_passes_on and + # generate_assembly_experimental are both destructive (mutating) on + # the IRContext + _helper1(vyper_source, optimize) + _helper2(vyper_source, optimize, compiler_settings) + + +def _helper1(vyper_source, optimize): + """ + Check that we are able to run passes on the round-tripped venom code + and that it is valid (generates bytecode) + """ + # note: compiling any later stage than bb_runtime like `asm` or + # `bytecode` modifies the bb_runtime data structure in place and results + # in normalization of the venom cfg (which breaks again make_ssa) out = compile_code(vyper_source, output_formats=["bb_runtime"]) + + bb_runtime = out["bb_runtime"] + venom_code = IRContext.__repr__(bb_runtime) + + ctx = parse_venom(venom_code) + + assert_ctx_eq(bb_runtime, ctx) + + # check it's valid to run venom passes+analyses + # (note this breaks bytecode equality, in the future we should + # test that separately) + run_passes_on(ctx, optimize) + + # test we can generate assembly+bytecode + asm = generate_assembly_experimental(ctx) + generate_bytecode(asm, compiler_metadata=None) + + +def _helper2(vyper_source, optimize, compiler_settings): + """ + Check that we can compile to bytecode, and without running venom passes, + that the output bytecode is equal to going through the normal vyper pipeline + """ + settings = copy.copy(compiler_settings) + # bytecode equivalence only makes sense if we use venom pipeline + settings.experimental_codegen = True + + out = compile_code(vyper_source, settings=settings, output_formats=["bb_runtime"]) bb_runtime = out["bb_runtime"] venom_code = IRContext.__repr__(bb_runtime) ctx = parse_venom(venom_code) assert_ctx_eq(bb_runtime, ctx) + + # test we can generate assembly+bytecode + asm = generate_assembly_experimental(ctx, optimize=optimize) + bytecode = generate_bytecode(asm, compiler_metadata=None) + + out = compile_code(vyper_source, settings=settings, output_formats=["bytecode_runtime"]) + assert "0x" + bytecode.hex() == out["bytecode_runtime"] diff --git a/tests/unit/compiler/venom/test_make_ssa.py b/tests/unit/compiler/venom/test_make_ssa.py index aa3fead6bf..7f6b2c0cba 100644 --- a/tests/unit/compiler/venom/test_make_ssa.py +++ b/tests/unit/compiler/venom/test_make_ssa.py @@ -1,48 +1,52 @@ +from tests.venom_utils import assert_ctx_eq, parse_venom from vyper.venom.analysis import IRAnalysesCache -from vyper.venom.basicblock import IRBasicBlock, IRLabel -from vyper.venom.context import IRContext from vyper.venom.passes import MakeSSA -def test_phi_case(): - ctx = IRContext() - fn = ctx.create_function("_global") - - bb = fn.get_basic_block() - - bb_cont = IRBasicBlock(IRLabel("condition"), fn) - bb_then = IRBasicBlock(IRLabel("then"), fn) - bb_else = IRBasicBlock(IRLabel("else"), fn) - bb_if_exit = IRBasicBlock(IRLabel("if_exit"), fn) - fn.append_basic_block(bb_cont) - fn.append_basic_block(bb_then) - fn.append_basic_block(bb_else) - fn.append_basic_block(bb_if_exit) - - v = bb.append_instruction("mload", 64) - bb_cont.append_instruction("jnz", v, bb_then.label, bb_else.label) - - bb_if_exit.append_instruction("add", v, 1, ret=v) - bb_if_exit.append_instruction("jmp", bb_cont.label) +def _check_pre_post(pre, post): + ctx = parse_venom(pre) + for fn in ctx.functions.values(): + ac = IRAnalysesCache(fn) + MakeSSA(ac, fn).run_pass() + assert_ctx_eq(ctx, parse_venom(post)) - bb_then.append_instruction("assert", bb_then.append_instruction("mload", 96)) - bb_then.append_instruction("jmp", bb_if_exit.label) - bb_else.append_instruction("jmp", bb_if_exit.label) - bb.append_instruction("jmp", bb_cont.label) - - ac = IRAnalysesCache(fn) - MakeSSA(ac, fn).run_pass() - - condition_block = fn.get_basic_block("condition") - assert len(condition_block.instructions) == 2 - - phi_inst = condition_block.instructions[0] - assert phi_inst.opcode == "phi" - assert phi_inst.operands[0].name == "_global" - assert phi_inst.operands[1].name == "%1" - assert phi_inst.operands[2].name == "if_exit" - assert phi_inst.operands[3].name == "%1" - assert phi_inst.output.name == "%1" - assert phi_inst.output.value != phi_inst.operands[1].value - assert phi_inst.output.value != phi_inst.operands[3].value +def test_phi_case(): + pre = """ + function loop { + main: + %v = mload 64 + jmp @test + test: + jnz %v, @then, @else + then: + %t = mload 96 + assert %t + jmp @if_exit + else: + jmp @if_exit + if_exit: + %v = add %v, 1 + jmp @test + } + """ + post = """ + function loop { + main: + %v = mload 64 + jmp @test + test: + %v:1 = phi @main, %v, @if_exit, %v:2 + jnz %v:1, @then, @else + then: + %t = mload 96 + assert %t + jmp @if_exit + else: + jmp @if_exit + if_exit: + %v:2 = add %v:1, 1 + jmp @test + } + """ + _check_pre_post(pre, post) diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index b0f0b00341..4c75c67700 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -117,7 +117,7 @@ class IROperand: """ value: Any - _hash: Optional[int] + _hash: Optional[int] = None def __init__(self, value: Any) -> None: self.value = value @@ -149,9 +149,8 @@ class IRLiteral(IROperand): value: int def __init__(self, value: int) -> None: - super().__init__(value) assert isinstance(value, int), "value must be an int" - self.value = value + super().__init__(value) class IRVariable(IROperand): @@ -163,17 +162,17 @@ class IRVariable(IROperand): version: Optional[int] def __init__(self, name: str, version: int = 0) -> None: - super().__init__(name) assert isinstance(name, str) - assert isinstance(version, int | None) + # TODO: allow version to be None + assert isinstance(version, int) if not name.startswith("%"): name = f"%{name}" self._name = name self.version = version + value = name if version > 0: - self.value = f"{name}:{version}" - else: - self.value = name + value = f"{name}:{version}" + super().__init__(value) @property def name(self) -> str: @@ -193,8 +192,8 @@ class IRLabel(IROperand): def __init__(self, value: str, is_symbol: bool = False) -> None: assert isinstance(value, str), f"not a str: {value} ({type(value)})" assert len(value) > 0 - super().__init__(value) self.is_symbol = is_symbol + super().__init__(value) _IS_IDENTIFIER = re.compile("[0-9a-zA-Z_]*") diff --git a/vyper/venom/passes/make_ssa.py b/vyper/venom/passes/make_ssa.py index 56d3e1b7d3..ee013e0f1d 100644 --- a/vyper/venom/passes/make_ssa.py +++ b/vyper/venom/passes/make_ssa.py @@ -35,8 +35,8 @@ def _add_phi_nodes(self): Add phi nodes to the function. """ self._compute_defs() - work = {var: 0 for var in self.dom.dfs_walk} - has_already = {var: 0 for var in self.dom.dfs_walk} + work = {bb: 0 for bb in self.dom.dfs_walk} + has_already = {bb: 0 for bb in self.dom.dfs_walk} i = 0 # Iterate over all variables @@ -96,7 +96,6 @@ def _rename_vars(self, basic_block: IRBasicBlock): self.var_name_counters[v_name] = i + 1 inst.output = IRVariable(v_name, version=i) - # note - after previous line, inst.output.name != v_name outs.append(inst.output.name) for bb in basic_block.cfg_out: @@ -106,8 +105,9 @@ def _rename_vars(self, basic_block: IRBasicBlock): assert inst.output is not None, "Phi instruction without output" for i, op in enumerate(inst.operands): if op == basic_block.label: + var = inst.operands[i + 1] inst.operands[i + 1] = IRVariable( - inst.output.name, version=self.var_name_stacks[inst.output.name][-1] + var.name, version=self.var_name_stacks[var.name][-1] ) for bb in self.dom.dominated[basic_block]: From 7caa05590a4c8788b01795d3e18a7aefe1183ebf Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 26 Dec 2024 15:28:59 -0500 Subject: [PATCH 2/6] refactor[venom]: refactor mem2var (#4421) remove special cases which were necessary before introduction of `palloca`. now they represent useless variable read/writes which are safe to remove. results in removal of a few instructions on benchmark contracts. --------- Co-authored-by: Harry Kalogirou --- vyper/venom/passes/mem2var.py | 51 ++++++++++++++++++----------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/vyper/venom/passes/mem2var.py b/vyper/venom/passes/mem2var.py index f93924d449..9f985e2b0b 100644 --- a/vyper/venom/passes/mem2var.py +++ b/vyper/venom/passes/mem2var.py @@ -34,31 +34,30 @@ def _mk_varname(self, varname: str): def _process_alloca_var(self, dfg: DFGAnalysis, var: IRVariable): """ - Process alloca allocated variable. If it is only used by mstore/mload/return - instructions, it is promoted to a stack variable. Otherwise, it is left as is. + Process alloca allocated variable. If it is only used by + mstore/mload/return instructions, it is promoted to a stack variable. + Otherwise, it is left as is. """ uses = dfg.get_uses(var) - if all([inst.opcode == "mload" for inst in uses]): - return - elif all([inst.opcode == "mstore" for inst in uses]): + if not all([inst.opcode in ["mstore", "mload", "return"] for inst in uses]): return - elif all([inst.opcode in ["mstore", "mload", "return"] for inst in uses]): - var_name = self._mk_varname(var.name) - for inst in uses: - if inst.opcode == "mstore": - inst.opcode = "store" - inst.output = IRVariable(var_name) - inst.operands = [inst.operands[0]] - elif inst.opcode == "mload": - inst.opcode = "store" - inst.operands = [IRVariable(var_name)] - elif inst.opcode == "return": - bb = inst.parent - idx = len(bb.instructions) - 1 - assert inst == bb.instructions[idx] # sanity - bb.insert_instruction( - IRInstruction("mstore", [IRVariable(var_name), inst.operands[1]]), idx - ) + + var_name = self._mk_varname(var.name) + var = IRVariable(var_name) + for inst in uses: + if inst.opcode == "mstore": + inst.opcode = "store" + inst.output = var + inst.operands = [inst.operands[0]] + elif inst.opcode == "mload": + inst.opcode = "store" + inst.operands = [var] + elif inst.opcode == "return": + bb = inst.parent + idx = len(bb.instructions) - 1 + assert inst == bb.instructions[idx] # sanity + new_inst = IRInstruction("mstore", [var, inst.operands[1]]) + bb.insert_instruction(new_inst, idx) def _process_palloca_var(self, dfg: DFGAnalysis, palloca_inst: IRInstruction, var: IRVariable): """ @@ -70,16 +69,18 @@ def _process_palloca_var(self, dfg: DFGAnalysis, palloca_inst: IRInstruction, va return var_name = self._mk_varname(var.name) + var = IRVariable(var_name) + # some value given to us by the calling convention palloca_inst.opcode = "mload" palloca_inst.operands = [palloca_inst.operands[0]] - palloca_inst.output = IRVariable(var_name) + palloca_inst.output = var for inst in uses: if inst.opcode == "mstore": inst.opcode = "store" - inst.output = IRVariable(var_name) + inst.output = var inst.operands = [inst.operands[0]] elif inst.opcode == "mload": inst.opcode = "store" - inst.operands = [IRVariable(var_name)] + inst.operands = [var] From 705aa54ed62606cdb868ebbfab0924e71f1a3f3f Mon Sep 17 00:00:00 2001 From: sandbubbles <160503471+sandbubbles@users.noreply.github.com> Date: Fri, 27 Dec 2024 21:10:30 +0100 Subject: [PATCH 3/6] feat[ux]: allow "compiling" `.vyi` files (#4290) This commit allows "compiling" `.vyi` files with ast, annotated_ast, interface, external_interface and abi output formats. Even though they don't produce bytecode, the above output formats can still be useful for users and tooling (even just to validate a `.vyi` file). --------- Co-authored-by: Charles Cooper --- tests/functional/codegen/test_interfaces.py | 105 ++++++++++++++++++ tests/unit/ast/test_ast_dict.py | 2 + .../cli/vyper_compile/test_compile_files.py | 29 +++++ vyper/ast/nodes.py | 2 +- vyper/ast/nodes.pyi | 1 + vyper/ast/parse.py | 8 +- vyper/compiler/__init__.py | 15 +++ vyper/compiler/output.py | 16 ++- vyper/compiler/phases.py | 3 + vyper/semantics/analysis/module.py | 2 +- vyper/semantics/types/user.py | 3 + 11 files changed, 179 insertions(+), 7 deletions(-) diff --git a/tests/functional/codegen/test_interfaces.py b/tests/functional/codegen/test_interfaces.py index e46a7d3dd4..e0b59ff668 100644 --- a/tests/functional/codegen/test_interfaces.py +++ b/tests/functional/codegen/test_interfaces.py @@ -876,3 +876,108 @@ def bar() -> uint256: input_bundle = make_input_bundle({"lib1.vy": lib1}) c = get_contract(main, input_bundle=input_bundle) assert c.bar() == 1 + + +def test_interface_with_flags(): + code = """ +struct MyStruct: + a: address + +flag Foo: + BOO + MOO + POO + +event Transfer: + sender: indexed(address) + +@external +def bar(): + pass +flag BAR: + BIZ + BAZ + BOO + +@external +@view +def foo(s: MyStruct) -> MyStruct: + return s + """ + + out = compile_code(code, contract_path="code.vy", output_formats=["interface"])["interface"] + + assert "# Flags" in out + assert "flag Foo:" in out + assert "flag BAR" in out + assert "BOO" in out + assert "MOO" in out + + compile_code(out, contract_path="code.vyi", output_formats=["interface"]) + + +vyi_filenames = [ + "test__test.vyi", + "test__t.vyi", + "t__test.vyi", + "t__t.vyi", + "t_t.vyi", + "test_test.vyi", + "t_test.vyi", + "test_t.vyi", + "_test_t__t_tt_.vyi", + "foo_bar_baz.vyi", +] + + +@pytest.mark.parametrize("vyi_filename", vyi_filenames) +def test_external_interface_names(vyi_filename): + code = """ +@external +def foo(): + ... + """ + + compile_code(code, contract_path=vyi_filename, output_formats=["external_interface"]) + + +def test_external_interface_with_flag(): + code = """ +flag Foo: + Blah + +@external +def foo() -> Foo: + ... + """ + + out = compile_code(code, contract_path="test__test.vyi", output_formats=["external_interface"])[ + "external_interface" + ] + assert "-> Foo:" in out + + +def test_external_interface_compiles_again(): + code = """ +@external +def foo() -> uint256: + ... +@external +def bar(a:int32) -> uint256: + ... + """ + + out = compile_code(code, contract_path="test.vyi", output_formats=["external_interface"])[ + "external_interface" + ] + compile_code(out, contract_path="test.vyi", output_formats=["external_interface"]) + + +@pytest.mark.xfail +def test_weird_interface_name(): + # based on comment https://github.com/vyperlang/vyper/pull/4290#discussion_r1884137428 + # we replace "_" for "" which results in an interface without name + out = compile_code("", contract_path="_.vyi", output_formats=["external_interface"])[ + "external_interface" + ] + assert "interface _:" in out diff --git a/tests/unit/ast/test_ast_dict.py b/tests/unit/ast/test_ast_dict.py index c9d7248809..196b1e24e6 100644 --- a/tests/unit/ast/test_ast_dict.py +++ b/tests/unit/ast/test_ast_dict.py @@ -399,6 +399,7 @@ def foo(): "node_id": 0, "path": "main.vy", "source_id": 1, + "is_interface": False, "type": { "name": "main.vy", "type_decl_node": {"node_id": 0, "source_id": 1}, @@ -1175,6 +1176,7 @@ def foo(): "node_id": 0, "path": "lib1.vy", "source_id": 0, + "is_interface": False, "type": { "name": "lib1.vy", "type_decl_node": {"node_id": 0, "source_id": 0}, diff --git a/tests/unit/cli/vyper_compile/test_compile_files.py b/tests/unit/cli/vyper_compile/test_compile_files.py index 3856aa3362..d8d9e56777 100644 --- a/tests/unit/cli/vyper_compile/test_compile_files.py +++ b/tests/unit/cli/vyper_compile/test_compile_files.py @@ -7,6 +7,7 @@ from vyper.cli.vyper_compile import compile_files from vyper.cli.vyper_json import compile_json +from vyper.compiler import INTERFACE_OUTPUT_FORMATS, OUTPUT_FORMATS from vyper.compiler.input_bundle import FilesystemInputBundle from vyper.compiler.output_bundle import OutputBundle from vyper.compiler.phases import CompilerData @@ -425,3 +426,31 @@ def test_archive_search_path(tmp_path_factory, make_file, chdir_tmp_path): used_dir = search_paths[-1].stem # either dir1 or dir2 assert output_bundle.used_search_paths == [".", "0/" + used_dir] + + +def test_compile_interface_file(make_file): + interface = """ +@view +@external +def foo() -> String[1]: + ... + +@view +@external +def bar() -> String[1]: + ... + +@external +def baz() -> uint8: + ... + + """ + file = make_file("interface.vyi", interface) + compile_files([file], INTERFACE_OUTPUT_FORMATS) + + # check unallowed output formats + for f in OUTPUT_FORMATS: + if f in INTERFACE_OUTPUT_FORMATS: + continue + with pytest.raises(ValueError): + compile_files([file], [f]) diff --git a/vyper/ast/nodes.py b/vyper/ast/nodes.py index 974685f403..ccc80947e4 100644 --- a/vyper/ast/nodes.py +++ b/vyper/ast/nodes.py @@ -638,7 +638,7 @@ class TopLevel(VyperNode): class Module(TopLevel): # metadata - __slots__ = ("path", "resolved_path", "source_id") + __slots__ = ("path", "resolved_path", "source_id", "is_interface") def to_dict(self): return dict(source_sha256sum=self.source_sha256sum, **super().to_dict()) diff --git a/vyper/ast/nodes.pyi b/vyper/ast/nodes.pyi index 783764271d..b00354c03a 100644 --- a/vyper/ast/nodes.pyi +++ b/vyper/ast/nodes.pyi @@ -71,6 +71,7 @@ class Module(TopLevel): path: str = ... resolved_path: str = ... source_id: int = ... + is_interface: bool = ... def namespace(self) -> Any: ... # context manager class FunctionDef(TopLevel): diff --git a/vyper/ast/parse.py b/vyper/ast/parse.py index d975aafac4..423b37721a 100644 --- a/vyper/ast/parse.py +++ b/vyper/ast/parse.py @@ -23,10 +23,11 @@ def parse_to_ast_with_settings( module_path: Optional[str] = None, resolved_path: Optional[str] = None, add_fn_node: Optional[str] = None, + is_interface: bool = False, ) -> tuple[Settings, vy_ast.Module]: try: return _parse_to_ast_with_settings( - vyper_source, source_id, module_path, resolved_path, add_fn_node + vyper_source, source_id, module_path, resolved_path, add_fn_node, is_interface ) except SyntaxException as e: e.resolved_path = resolved_path @@ -39,6 +40,7 @@ def _parse_to_ast_with_settings( module_path: Optional[str] = None, resolved_path: Optional[str] = None, add_fn_node: Optional[str] = None, + is_interface: bool = False, ) -> tuple[Settings, vy_ast.Module]: """ Parses a Vyper source string and generates basic Vyper AST nodes. @@ -62,6 +64,9 @@ def _parse_to_ast_with_settings( resolved_path: str, optional The resolved path of the source code Corresponds to FileInput.resolved_path + is_interface: bool + Indicates whether the source code should + be parsed as an interface file. Returns ------- @@ -106,6 +111,7 @@ def _parse_to_ast_with_settings( # Convert to Vyper AST. module = vy_ast.get_node(py_ast) assert isinstance(module, vy_ast.Module) # mypy hint + module.is_interface = is_interface return pre_parser.settings, module diff --git a/vyper/compiler/__init__.py b/vyper/compiler/__init__.py index d885599cec..57bd2f4096 100644 --- a/vyper/compiler/__init__.py +++ b/vyper/compiler/__init__.py @@ -46,6 +46,13 @@ "opcodes_runtime": output.build_opcodes_runtime_output, } +INTERFACE_OUTPUT_FORMATS = [ + "ast_dict", + "annotated_ast_dict", + "interface", + "external_interface", + "abi", +] UNKNOWN_CONTRACT_NAME = "" @@ -121,10 +128,18 @@ def outputs_from_compiler_data( output_formats = ("bytecode",) ret = {} + with anchor_settings(compiler_data.settings): for output_format in output_formats: if output_format not in OUTPUT_FORMATS: raise ValueError(f"Unsupported format type {repr(output_format)}") + + is_vyi = compiler_data.file_input.resolved_path.suffix == ".vyi" + if is_vyi and output_format not in INTERFACE_OUTPUT_FORMATS: + raise ValueError( + f"Unsupported format for compiling interface: {repr(output_format)}" + ) + try: formatter = OUTPUT_FORMATS[output_format] ret[output_format] = formatter(compiler_data) diff --git a/vyper/compiler/output.py b/vyper/compiler/output.py index 1a6b3e9c07..ca951b8e39 100644 --- a/vyper/compiler/output.py +++ b/vyper/compiler/output.py @@ -108,9 +108,8 @@ def build_integrity(compiler_data: CompilerData) -> str: def build_external_interface_output(compiler_data: CompilerData) -> str: interface = compiler_data.annotated_vyper_module._metadata["type"].interface stem = PurePath(compiler_data.contract_path).stem - # capitalize words separated by '_' - # ex: test_interface.vy -> TestInterface - name = "".join([x.capitalize() for x in stem.split("_")]) + + name = stem.title().replace("_", "") out = f"\n# External Interfaces\ninterface {name}:\n" for func in interface.functions.values(): @@ -136,6 +135,14 @@ def build_interface_output(compiler_data: CompilerData) -> str: out += f" {member_name}: {member_type}\n" out += "\n\n" + if len(interface.flags) > 0: + out += "# Flags\n\n" + for flag in interface.flags.values(): + out += f"flag {flag.name}:\n" + for flag_value in flag._flag_members: + out += f" {flag_value}\n" + out += "\n\n" + if len(interface.events) > 0: out += "# Events\n\n" for event in interface.events.values(): @@ -282,7 +289,8 @@ def build_method_identifiers_output(compiler_data: CompilerData) -> dict: def build_abi_output(compiler_data: CompilerData) -> list: module_t = compiler_data.annotated_vyper_module._metadata["type"] - _ = compiler_data.ir_runtime # ensure _ir_info is generated + if not compiler_data.annotated_vyper_module.is_interface: + _ = compiler_data.ir_runtime # ensure _ir_info is generated abi = module_t.interface.to_toplevel_abi_dict() if module_t.init_function: diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index 503281a867..4925d9971c 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -113,11 +113,14 @@ def contract_path(self): @cached_property def _generate_ast(self): + is_vyi = self.contract_path.suffix == ".vyi" + settings, ast = vy_ast.parse_to_ast_with_settings( self.source_code, self.source_id, module_path=self.contract_path.as_posix(), resolved_path=self.file_input.resolved_path.as_posix(), + is_interface=is_vyi, ) if self.original_settings: diff --git a/vyper/semantics/analysis/module.py b/vyper/semantics/analysis/module.py index 737f675b7c..534af4d633 100644 --- a/vyper/semantics/analysis/module.py +++ b/vyper/semantics/analysis/module.py @@ -53,7 +53,7 @@ def analyze_module(module_ast: vy_ast.Module) -> ModuleT: add all module-level objects to the namespace, type-check/validate semantics and annotate with type and analysis info """ - return _analyze_module_r(module_ast) + return _analyze_module_r(module_ast, module_ast.is_interface) def _analyze_module_r(module_ast: vy_ast.Module, is_interface: bool = False): diff --git a/vyper/semantics/types/user.py b/vyper/semantics/types/user.py index 73fa4878c7..d01ab23299 100644 --- a/vyper/semantics/types/user.py +++ b/vyper/semantics/types/user.py @@ -77,6 +77,9 @@ def get_type_member(self, key: str, node: vy_ast.VyperNode) -> "VyperType": self._helper.get_member(key, node) return self + def __str__(self): + return f"{self.name}" + def __repr__(self): arg_types = ",".join(repr(a) for a in self._flag_members) return f"flag {self.name}({arg_types})" From e9ac7cd623ebab98820cbafc93b8b4c1746d7070 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 28 Dec 2024 07:57:32 -0500 Subject: [PATCH 4/6] fix[stdlib]: fix `IERC4626` signatures (#4425) the signatures should not have default parameters; if used, they will create a different method id at the callsite than the ERC4626 standard actually accepts. - add `stdlib` to the list of valid PR scopes --- .github/workflows/pull-request.yaml | 2 ++ vyper/builtins/interfaces/IERC4626.vyi | 8 ++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pull-request.yaml b/.github/workflows/pull-request.yaml index 2eb0113487..a2f4b5a0d1 100644 --- a/.github/workflows/pull-request.yaml +++ b/.github/workflows/pull-request.yaml @@ -32,6 +32,7 @@ jobs: # docs: documentation # test: test suite # lang: language changes + # stdlib: changes to the stdlib # ux: language changes (UX) # tool: integration # ir: (old) IR/codegen changes @@ -43,6 +44,7 @@ jobs: docs test lang + stdlib ux tool ir diff --git a/vyper/builtins/interfaces/IERC4626.vyi b/vyper/builtins/interfaces/IERC4626.vyi index 6d9e4c6ef7..0dd398d1f3 100644 --- a/vyper/builtins/interfaces/IERC4626.vyi +++ b/vyper/builtins/interfaces/IERC4626.vyi @@ -44,7 +44,7 @@ def previewDeposit(assets: uint256) -> uint256: ... @external -def deposit(assets: uint256, receiver: address=msg.sender) -> uint256: +def deposit(assets: uint256, receiver: address) -> uint256: ... @view @@ -58,7 +58,7 @@ def previewMint(shares: uint256) -> uint256: ... @external -def mint(shares: uint256, receiver: address=msg.sender) -> uint256: +def mint(shares: uint256, receiver: address) -> uint256: ... @view @@ -72,7 +72,7 @@ def previewWithdraw(assets: uint256) -> uint256: ... @external -def withdraw(assets: uint256, receiver: address=msg.sender, owner: address=msg.sender) -> uint256: +def withdraw(assets: uint256, receiver: address, owner: address) -> uint256: ... @view @@ -86,5 +86,5 @@ def previewRedeem(shares: uint256) -> uint256: ... @external -def redeem(shares: uint256, receiver: address=msg.sender, owner: address=msg.sender) -> uint256: +def redeem(shares: uint256, receiver: address, owner: address) -> uint256: ... From 614ea0da18110b9be4192dd96f3baad695b9e888 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 28 Dec 2024 16:45:49 -0500 Subject: [PATCH 5/6] fix[venom]: fix store elimination pass (#4428) this commit fixes the store elimination pass by updating the dfg in-place instead of relying on a stale dfg. this currently results in no bytecode changes. previously this was undetected because the order of items in the dfg happens to be "well-behaved", but if the dfg is built using a traversal of basic blocks in a different order (as may happen in upcoming passes), it can result in store instructions failing to be eliminated. note that we haven't rebuilt the dfg properly because `dfg.outputs` is invalid after this pass. we could modify `dfg.outputs` in place, but that results in a bytecode regression. this commit also removes the dependency on CFGAnalysis as it is not actually needed by the pass. --------- Co-authored-by: Harry Kalogirou --- vyper/venom/passes/store_elimination.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/vyper/venom/passes/store_elimination.py b/vyper/venom/passes/store_elimination.py index 22d4723013..a4f217505b 100644 --- a/vyper/venom/passes/store_elimination.py +++ b/vyper/venom/passes/store_elimination.py @@ -1,4 +1,4 @@ -from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, LivenessAnalysis +from vyper.venom.analysis import DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRVariable from vyper.venom.passes.base_pass import IRPass @@ -13,31 +13,33 @@ class StoreElimination(IRPass): # with LoadElimination def run_pass(self): - self.analyses_cache.request_analysis(CFGAnalysis) - dfg = self.analyses_cache.request_analysis(DFGAnalysis) + self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) - for var, inst in dfg.outputs.items(): + for var, inst in self.dfg.outputs.items(): if inst.opcode != "store": continue - self._process_store(dfg, inst, var, inst.operands[0]) + self._process_store(inst, var, inst.operands[0]) self.analyses_cache.invalidate_analysis(LivenessAnalysis) self.analyses_cache.invalidate_analysis(DFGAnalysis) - def _process_store(self, dfg, inst, var: IRVariable, new_var: IRVariable): + def _process_store(self, inst, var: IRVariable, new_var: IRVariable): """ Process store instruction. If the variable is only used by a load instruction, forward the variable to the load instruction. """ - if any([inst.opcode == "phi" for inst in dfg.get_uses(new_var)]): + if any([inst.opcode == "phi" for inst in self.dfg.get_uses(new_var)]): return - uses = dfg.get_uses(var) + uses = self.dfg.get_uses(var) if any([inst.opcode == "phi" for inst in uses]): return - for use_inst in uses: + for use_inst in uses.copy(): for i, operand in enumerate(use_inst.operands): if operand == var: use_inst.operands[i] = new_var + self.dfg.add_use(new_var, use_inst) + self.dfg.remove_use(var, use_inst) + inst.parent.remove_instruction(inst) From 194d60ac6dbfe32d17d0073777817e8d95107d74 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sun, 29 Dec 2024 15:35:15 -0500 Subject: [PATCH 6/6] fix[tool]: fix output formats for .vyz files (#4338) run the format name through the translate map. for instance, `annotated_ast` output format was not working for `.vyz` files. this commit has some additional fixes that were discovered when adding the integration tests and refactoring related to the settings not getting propagated uniformly across different entry points. --------- Co-authored-by: cyberthirst --- tests/functional/venom/test_venom_repr.py | 16 ++- .../cli/vyper_compile/test_compile_files.py | 99 +++++++++++++++++++ .../unit/cli/vyper_json/test_compile_json.py | 3 +- tests/unit/compiler/test_bytecode_runtime.py | 20 +++- vyper/cli/vyper_compile.py | 2 +- vyper/cli/vyper_json.py | 11 ++- vyper/compiler/phases.py | 2 + vyper/compiler/settings.py | 12 +-- vyper/ir/compile_ir.py | 5 + 9 files changed, 155 insertions(+), 15 deletions(-) diff --git a/tests/functional/venom/test_venom_repr.py b/tests/functional/venom/test_venom_repr.py index 5136672a03..1fb5d0486a 100644 --- a/tests/functional/venom/test_venom_repr.py +++ b/tests/functional/venom/test_venom_repr.py @@ -7,6 +7,7 @@ from tests.venom_utils import assert_ctx_eq, parse_venom from vyper.compiler import compile_code from vyper.compiler.phases import generate_bytecode +from vyper.compiler.settings import OptimizationLevel from vyper.venom import generate_assembly_experimental, run_passes_on from vyper.venom.context import IRContext @@ -20,7 +21,7 @@ def get_example_vy_filenames(): @pytest.mark.parametrize("vy_filename", get_example_vy_filenames()) -def test_round_trip_examples(vy_filename, optimize, compiler_settings): +def test_round_trip_examples(vy_filename, debug, optimize, compiler_settings, request): """ Check all examples round trip """ @@ -28,6 +29,11 @@ def test_round_trip_examples(vy_filename, optimize, compiler_settings): with open(path) as f: vyper_source = f.read() + if debug and optimize == OptimizationLevel.CODESIZE: + # FIXME: some round-trips fail when debug is enabled due to labels + # not getting pinned + request.node.add_marker(pytest.mark.xfail(strict=False)) + _round_trip_helper(vyper_source, optimize, compiler_settings) @@ -45,11 +51,17 @@ def _loop() -> uint256: @pytest.mark.parametrize("vyper_source", vyper_sources) -def test_round_trip_sources(vyper_source, optimize, compiler_settings): +def test_round_trip_sources(vyper_source, debug, optimize, compiler_settings, request): """ Test vyper_sources round trip """ vyper_source = textwrap.dedent(vyper_source) + + if debug and optimize == OptimizationLevel.CODESIZE: + # FIXME: some round-trips fail when debug is enabled due to labels + # not getting pinned + request.node.add_marker(pytest.mark.xfail(strict=False)) + _round_trip_helper(vyper_source, optimize, compiler_settings) diff --git a/tests/unit/cli/vyper_compile/test_compile_files.py b/tests/unit/cli/vyper_compile/test_compile_files.py index d8d9e56777..0fd938d519 100644 --- a/tests/unit/cli/vyper_compile/test_compile_files.py +++ b/tests/unit/cli/vyper_compile/test_compile_files.py @@ -361,6 +361,105 @@ def test_archive_b64_output(input_files): assert out[contract_file] == out2[archive_path] +def test_archive_compile_options(input_files): + tmpdir, _, _, contract_file = input_files + search_paths = [".", tmpdir] + + options = ["abi_python", "json", "ast", "annotated_ast", "ir_json"] + + for option in options: + out = compile_files([contract_file], ["archive_b64", option], paths=search_paths) + + archive_b64 = out[contract_file].pop("archive_b64") + + archive_path = Path("foo.zip.b64") + with archive_path.open("w") as f: + f.write(archive_b64) + + # compare compiling the two input bundles + out2 = compile_files([archive_path], [option]) + + if option in ["ast", "annotated_ast"]: + # would have to normalize paths and imports, so just verify it compiles + continue + + assert out[contract_file] == out2[archive_path] + + +format_options = [ + "bytecode", + "bytecode_runtime", + "blueprint_bytecode", + "abi", + "abi_python", + "source_map", + "source_map_runtime", + "method_identifiers", + "userdoc", + "devdoc", + "metadata", + "combined_json", + "layout", + "ast", + "annotated_ast", + "interface", + "external_interface", + "opcodes", + "opcodes_runtime", + "ir", + "ir_json", + "ir_runtime", + "asm", + "integrity", + "archive", + "solc_json", +] + + +def test_compile_vyz_with_options(input_files): + tmpdir, _, _, contract_file = input_files + search_paths = [".", tmpdir] + + for option in format_options: + out_archive = compile_files([contract_file], ["archive"], paths=search_paths) + + archive = out_archive[contract_file].pop("archive") + + archive_path = Path("foo.zip.out.vyz") + with archive_path.open("wb") as f: + f.write(archive) + + # compare compiling the two input bundles + out = compile_files([contract_file], [option], paths=search_paths) + out2 = compile_files([archive_path], [option]) + + if option in ["ast", "annotated_ast", "metadata"]: + # would have to normalize paths and imports, so just verify it compiles + continue + + if option in ["ir_runtime", "ir", "archive"]: + # ir+ir_runtime is different due to being different compiler runs + # archive is different due to different metadata (timestamps) + continue + + assert out[contract_file] == out2[archive_path] + + +def test_archive_compile_simultaneous_options(input_files): + tmpdir, _, _, contract_file = input_files + search_paths = [".", tmpdir] + + for option in format_options: + with pytest.raises(ValueError) as e: + _ = compile_files([contract_file], ["archive", option], paths=search_paths) + + err_opt = "archive" + if option in ("combined_json", "solc_json"): + err_opt = option + + assert f"If using {err_opt} it must be the only output format requested" in str(e.value) + + def test_solc_json_output(input_files): tmpdir, _, _, contract_file = input_files search_paths = [".", tmpdir] diff --git a/tests/unit/cli/vyper_json/test_compile_json.py b/tests/unit/cli/vyper_json/test_compile_json.py index 7e281bda2e..9044148aa9 100644 --- a/tests/unit/cli/vyper_json/test_compile_json.py +++ b/tests/unit/cli/vyper_json/test_compile_json.py @@ -73,7 +73,7 @@ def oopsie(a: uint256) -> bool: @pytest.fixture(scope="function") -def input_json(optimize, evm_version, experimental_codegen): +def input_json(optimize, evm_version, experimental_codegen, debug): return { "language": "Vyper", "sources": { @@ -87,6 +87,7 @@ def input_json(optimize, evm_version, experimental_codegen): "optimize": optimize.name.lower(), "evmVersion": evm_version, "experimentalCodegen": experimental_codegen, + "debug": debug, }, } diff --git a/tests/unit/compiler/test_bytecode_runtime.py b/tests/unit/compiler/test_bytecode_runtime.py index 1d38130c49..9fdc4c493f 100644 --- a/tests/unit/compiler/test_bytecode_runtime.py +++ b/tests/unit/compiler/test_bytecode_runtime.py @@ -54,7 +54,7 @@ def test_bytecode_runtime(): assert out["bytecode_runtime"].removeprefix("0x") in out["bytecode"].removeprefix("0x") -def test_bytecode_signature(): +def test_bytecode_signature(optimize, debug): out = vyper.compile_code( simple_contract_code, output_formats=["bytecode_runtime", "bytecode", "integrity"] ) @@ -65,10 +65,16 @@ def test_bytecode_signature(): metadata = _parse_cbor_metadata(initcode) integrity_hash, runtime_len, data_section_lengths, immutables_len, compiler = metadata + if debug and optimize == OptimizationLevel.CODESIZE: + # debug forces dense jumptable no matter the size of selector table + expected_data_section_lengths = [5, 7] + else: + expected_data_section_lengths = [] + assert integrity_hash.hex() == out["integrity"] assert runtime_len == len(runtime_code) - assert data_section_lengths == [] + assert data_section_lengths == expected_data_section_lengths assert immutables_len == 0 assert compiler == {"vyper": list(vyper.version.version_tuple)} @@ -119,7 +125,7 @@ def test_bytecode_signature_sparse_jumptable(): assert compiler == {"vyper": list(vyper.version.version_tuple)} -def test_bytecode_signature_immutables(): +def test_bytecode_signature_immutables(debug, optimize): out = vyper.compile_code( has_immutables, output_formats=["bytecode_runtime", "bytecode", "integrity"] ) @@ -130,10 +136,16 @@ def test_bytecode_signature_immutables(): metadata = _parse_cbor_metadata(initcode) integrity_hash, runtime_len, data_section_lengths, immutables_len, compiler = metadata + if debug and optimize == OptimizationLevel.CODESIZE: + # debug forces dense jumptable no matter the size of selector table + expected_data_section_lengths = [5, 7] + else: + expected_data_section_lengths = [] + assert integrity_hash.hex() == out["integrity"] assert runtime_len == len(runtime_code) - assert data_section_lengths == [] + assert data_section_lengths == expected_data_section_lengths assert immutables_len == 32 assert compiler == {"vyper": list(vyper.version.version_tuple)} diff --git a/vyper/cli/vyper_compile.py b/vyper/cli/vyper_compile.py index 046cac2c0b..390416799a 100755 --- a/vyper/cli/vyper_compile.py +++ b/vyper/cli/vyper_compile.py @@ -359,7 +359,7 @@ def compile_files( # we allow this instead of requiring a different mode (like # `--zip`) so that verifier pipelines do not need a different # workflow for archive files and single-file contracts. - output = compile_from_zip(file_name, output_formats, settings, no_bytecode_metadata) + output = compile_from_zip(file_name, final_formats, settings, no_bytecode_metadata) ret[file_path] = output continue except NotZipInput: diff --git a/vyper/cli/vyper_json.py b/vyper/cli/vyper_json.py index 9fcdf27baf..e7704b9398 100755 --- a/vyper/cli/vyper_json.py +++ b/vyper/cli/vyper_json.py @@ -272,8 +272,17 @@ def get_settings(input_dict: dict) -> Settings: else: assert optimize is None + debug = input_dict["settings"].get("debug", None) + + # TODO: maybe change these to camelCase for consistency + enable_decimals = input_dict["settings"].get("enable_decimals", None) + return Settings( - evm_version=evm_version, optimize=optimize, experimental_codegen=experimental_codegen + evm_version=evm_version, + optimize=optimize, + experimental_codegen=experimental_codegen, + debug=debug, + enable_decimals=enable_decimals, ) diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index 4925d9971c..e6cb1c58d6 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -12,6 +12,7 @@ from vyper.compiler.input_bundle import FileInput, FilesystemInputBundle, InputBundle from vyper.compiler.settings import OptimizationLevel, Settings, anchor_settings, merge_settings from vyper.ir import compile_ir, optimizer +from vyper.ir.compile_ir import reset_symbols from vyper.semantics import analyze_module, set_data_positions, validate_compilation_target from vyper.semantics.analysis.data_positions import generate_layout_export from vyper.semantics.analysis.imports import resolve_imports @@ -310,6 +311,7 @@ def generate_ir_nodes(global_ctx: ModuleT, settings: Settings) -> tuple[IRnode, """ # make IR output the same between runs codegen.reset_names() + reset_symbols() with anchor_settings(settings): ir_nodes, ir_runtime = module.generate_ir_for_module(global_ctx) diff --git a/vyper/compiler/settings.py b/vyper/compiler/settings.py index a8e28c1ed1..e9840e8334 100644 --- a/vyper/compiler/settings.py +++ b/vyper/compiler/settings.py @@ -120,12 +120,12 @@ def _merge_one(lhs, rhs, helpstr): return lhs if rhs is None else rhs ret = Settings() - ret.evm_version = _merge_one(one.evm_version, two.evm_version, "evm version") - ret.optimize = _merge_one(one.optimize, two.optimize, "optimize") - ret.experimental_codegen = _merge_one( - one.experimental_codegen, two.experimental_codegen, "experimental codegen" - ) - ret.enable_decimals = _merge_one(one.enable_decimals, two.enable_decimals, "enable-decimals") + for field in dataclasses.fields(ret): + if field.name == "compiler_version": + continue + pretty_name = field.name.replace("_", "-") # e.g. evm_version -> evm-version + val = _merge_one(getattr(one, field.name), getattr(two, field.name), pretty_name) + setattr(ret, field.name, val) return ret diff --git a/vyper/ir/compile_ir.py b/vyper/ir/compile_ir.py index e87cf1b310..936e6d5d72 100644 --- a/vyper/ir/compile_ir.py +++ b/vyper/ir/compile_ir.py @@ -54,6 +54,11 @@ def mksymbol(name=""): return f"_sym_{name}{_next_symbol}" +def reset_symbols(): + global _next_symbol + _next_symbol = 0 + + def mkdebug(pc_debugger, ast_source): i = Instruction("DEBUG", ast_source) i.pc_debugger = pc_debugger