From 28b65911517934eab8071888425d601fe5636daa Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 13:37:10 +0200 Subject: [PATCH 01/59] inliner --- tests/unit/compiler/asm/test_asm_optimizer.py | 4 +- tests/unit/compiler/test_source_map.py | 9 +- tests/unit/compiler/venom/test_inliner.py | 64 ++++++ .../venom/test_variable_equivalence.py | 38 ++++ vyper/codegen/context.py | 3 + .../function_definitions/internal_function.py | 5 + vyper/codegen/self_call.py | 27 ++- vyper/compiler/phases.py | 2 +- vyper/utils.py | 11 ++ vyper/venom/README.md | 5 + vyper/venom/__init__.py | 28 ++- vyper/venom/analysis/__init__.py | 1 - vyper/venom/analysis/analysis.py | 4 + vyper/venom/analysis/cfg.py | 35 +++- vyper/venom/analysis/dfg.py | 19 +- vyper/venom/analysis/dominators.py | 22 +-- vyper/venom/analysis/equivalent_vars.py | 35 ++-- vyper/venom/analysis/fcg.py | 49 +++++ vyper/venom/analysis/liveness.py | 2 +- vyper/venom/basicblock.py | 118 +++++++++-- vyper/venom/context.py | 29 ++- vyper/venom/function.py | 45 ++++- vyper/venom/ir_node_to_venom.py | 101 ++++++++-- vyper/venom/parser.py | 4 +- vyper/venom/passes/__init__.py | 1 + vyper/venom/passes/base_pass.py | 28 +++ vyper/venom/passes/float_allocas.py | 2 +- vyper/venom/passes/func_inliner.py | 184 ++++++++++++++++++ vyper/venom/passes/load_elimination.py | 7 +- vyper/venom/passes/make_ssa.py | 6 +- vyper/venom/passes/mem2var.py | 21 +- vyper/venom/passes/sccp/sccp.py | 7 +- vyper/venom/venom_to_assembly.py | 21 +- 33 files changed, 818 insertions(+), 119 deletions(-) create mode 100644 tests/unit/compiler/venom/test_inliner.py create mode 100644 tests/unit/compiler/venom/test_variable_equivalence.py create mode 100644 vyper/venom/analysis/fcg.py create mode 100644 vyper/venom/passes/func_inliner.py diff --git a/tests/unit/compiler/asm/test_asm_optimizer.py b/tests/unit/compiler/asm/test_asm_optimizer.py index ee6b1653b0..dfbb53ad5a 100644 --- a/tests/unit/compiler/asm/test_asm_optimizer.py +++ b/tests/unit/compiler/asm/test_asm_optimizer.py @@ -120,7 +120,9 @@ def foo(): input_bundle = make_input_bundle({"library.vy": library}) res = compile_code(code, input_bundle=input_bundle, output_formats=["asm"]) asm = res["asm"] - assert "some_function()" in asm + + if not experimental_codegen: + assert "some_function()" in asm # Venom function inliner will remove this assert "unused1()" not in asm assert "unused2()" not in asm diff --git a/tests/unit/compiler/test_source_map.py b/tests/unit/compiler/test_source_map.py index ae1999a26e..974e4f2ece 100644 --- a/tests/unit/compiler/test_source_map.py +++ b/tests/unit/compiler/test_source_map.py @@ -38,6 +38,7 @@ def test_jump_map(optimize, experimental_codegen): jump_map = source_map["pc_jump_map"] expected_jumps = 1 + expected_internals = 2 if optimize == OptimizationLevel.NONE: # some jumps which don't get optimized out when optimizer is off # (slightly different behavior depending if venom pipeline is enabled): @@ -45,9 +46,15 @@ def test_jump_map(optimize, experimental_codegen): expected_jumps = 3 else: expected_jumps = 2 + else: + if not experimental_codegen: + expected_internals = 2 + else: + expected_jumps = 0 + expected_internals = 0 assert len([v for v in jump_map.values() if v == "o"]) == expected_jumps - assert len([v for v in jump_map.values() if v == "i"]) == 2 + assert len([v for v in jump_map.values() if v == "i"]) == expected_internals code_lines = [i + "\n" for i in TEST_CODE.split("\n")] for pc in [k for k, v in jump_map.items() if v == "o"]: diff --git a/tests/unit/compiler/venom/test_inliner.py b/tests/unit/compiler/venom/test_inliner.py new file mode 100644 index 0000000000..9b6690a15e --- /dev/null +++ b/tests/unit/compiler/venom/test_inliner.py @@ -0,0 +1,64 @@ +import pytest + +from tests.evm_backends.base_env import ExecutionReverted + + +def test_call_in_call(get_contract): + code = """ +@internal +def _foo(a: uint256,) -> uint256: + return 1 + a + +@internal +def _foo2() -> uint256: + return 4 + +@external +def foo() -> uint256: + return self._foo(self._foo2()) + """ + + c = get_contract(code) + assert c.foo() == 5 + + +def test_call_in_call_with_raise(get_contract): + code = """ +@internal +def sum(a: uint256) -> uint256: + if a > 1: + return a + 1 + raise + +@internal +def middle(a: uint256) -> uint256: + return self.sum(a) + +@external +def test(a: uint256) -> uint256: + return self.middle(a) + """ + + c = get_contract(code) + + assert c.test(2) == 3 + + with pytest.raises(ExecutionReverted): + c.test(0) + + +def test_inliner_with_unused_param(get_contract): + code = """ +data: public(uint256) + +@internal +def _foo(start: uint256, length: uint256): + self.data = start + +@external +def foo(x: uint256, y: uint256): + self._foo(x, y) +""" + + c = get_contract(code) + c.foo(1, 2) diff --git a/tests/unit/compiler/venom/test_variable_equivalence.py b/tests/unit/compiler/venom/test_variable_equivalence.py new file mode 100644 index 0000000000..717c026dff --- /dev/null +++ b/tests/unit/compiler/venom/test_variable_equivalence.py @@ -0,0 +1,38 @@ +import itertools + +from tests.venom_utils import parse_from_basic_block +from vyper.venom.analysis import DFGAnalysis, IRAnalysesCache +from vyper.venom.basicblock import IRVariable +from vyper.venom.context import IRContext + + +def _entry_fn(ctx: "IRContext"): + # TODO: make this part of IRContext + return next(iter(ctx.functions.values())) + + +def test_variable_equivalence_dfg_order(): + a_code = """ + main: + %1 = 1 + %2 = %1 + %3 = %2 + """ + # technically invalid code, but variable equivalence should handle + # it either way + b_code = """ + main: + %3 = %2 + %2 = %1 + %1 = 1 + """ + fn1 = _entry_fn(parse_from_basic_block(a_code)) + fn2 = _entry_fn(parse_from_basic_block(b_code)) + + dfg1 = IRAnalysesCache(fn1).request_analysis(DFGAnalysis) + dfg2 = IRAnalysesCache(fn2).request_analysis(DFGAnalysis) + + vars_ = map(IRVariable, ("%1", "%2", "%3")) + for var1, var2 in itertools.combinations(vars_, 2): + assert dfg1.are_equivalent(var1, var2) + assert dfg2.are_equivalent(var1, var2) diff --git a/vyper/codegen/context.py b/vyper/codegen/context.py index 7995b7b9f5..9562db7f03 100644 --- a/vyper/codegen/context.py +++ b/vyper/codegen/context.py @@ -35,6 +35,9 @@ class Alloca: _id: int + # special metadata for calloca. hint for venom to tie calloca to call site. + _callsite: Optional[str] = None + def __post_init__(self): assert self.typ.memory_bytes_required == self.size diff --git a/vyper/codegen/function_definitions/internal_function.py b/vyper/codegen/function_definitions/internal_function.py index e98c8a5632..6d6cf8d12e 100644 --- a/vyper/codegen/function_definitions/internal_function.py +++ b/vyper/codegen/function_definitions/internal_function.py @@ -78,6 +78,11 @@ def generate_ir_for_internal_function( ir_node = IRnode.from_list(["seq", body, cleanup_routine]) + # add function signature to passthru metadata so that venom + # has more information to work with + ir_node.passthrough_metadata["func_t"] = func_t + ir_node.passthrough_metadata["context"] = context + # tag gas estimate and frame info func_t._ir_info.gas_estimate = ir_node.gas tag_frame_info(func_t, context) diff --git a/vyper/codegen/self_call.py b/vyper/codegen/self_call.py index fef6070d14..32e5c84088 100644 --- a/vyper/codegen/self_call.py +++ b/vyper/codegen/self_call.py @@ -1,3 +1,6 @@ +import copy +import dataclasses + from vyper.codegen.core import _freshname, eval_once_check, make_setter from vyper.codegen.ir_node import IRnode from vyper.evm.address_space import MEMORY @@ -66,7 +69,27 @@ def ir_for_self_call(stmt_expr, context): # note: dst_tuple_t != args_tuple_t dst_tuple_t = TupleT(tuple(func_t.argument_types)) - args_dst = IRnode(func_t._ir_info.frame_info.frame_start, typ=dst_tuple_t, location=MEMORY) + if context.settings.experimental_codegen: + arg_items = ["multi"] + frame_info = func_t._ir_info.frame_info + + for var in frame_info.frame_vars.values(): + var = copy.copy(var) + alloca = var.alloca + assert alloca is not None + assert isinstance(var.pos, str) # help mypy + if not var.pos.startswith("$palloca"): + continue + newname = var.pos.replace("$palloca", "$calloca") + var.pos = newname + alloca = dataclasses.replace(alloca, _callsite=return_label) + irnode = var.as_ir_node() + irnode.passthrough_metadata["alloca"] = alloca + arg_items.append(irnode) + args_dst = IRnode.from_list(arg_items, typ=dst_tuple_t) + else: + # legacy + args_dst = IRnode(func_t._ir_info.frame_info.frame_start, typ=dst_tuple_t, location=MEMORY) # if one of the arguments is a self call, the argument # buffer could get borked. to prevent against that, @@ -109,4 +132,6 @@ def ir_for_self_call(stmt_expr, context): ) o.is_self_call = True o.invoked_function_ir = func_t._ir_info.func_ir + o.passthrough_metadata["func_t"] = func_t + o.passthrough_metadata["args_ir"] = args_ir return o diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index 17812ee535..a5e17e1411 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -328,7 +328,7 @@ def generate_ir_nodes(global_ctx: ModuleT, settings: Settings) -> tuple[IRnode, with anchor_settings(settings): ir_nodes, ir_runtime = module.generate_ir_for_module(global_ctx) - if settings.optimize != OptimizationLevel.NONE: + if settings.optimize != OptimizationLevel.NONE and not settings.experimental_codegen: ir_nodes = optimizer.optimize(ir_nodes) ir_runtime = optimizer.optimize(ir_runtime) return ir_nodes, ir_runtime diff --git a/vyper/utils.py b/vyper/utils.py index 39d3093478..f1d84d5fd4 100644 --- a/vyper/utils.py +++ b/vyper/utils.py @@ -693,3 +693,14 @@ def safe_relpath(path): # on Windows, if path and curdir are on different drives, an exception # can be thrown return path + + +def all_nonempty(iter): + """ + This function checks if all elements in the given `iterable` are truthy, + similar to Python's built-in `all()` function. However, `all_nonempty` + diverges by returning `False` if the iterable is empty, whereas `all()` + would return `True` for an empty iterable. + """ + items = list(iter) + return len(items) > 0 and all(items) diff --git a/vyper/venom/README.md b/vyper/venom/README.md index 964f52b524..ff5e2888a4 100644 --- a/vyper/venom/README.md +++ b/vyper/venom/README.md @@ -205,6 +205,11 @@ Assembly can be inspected with `-f asm`, whereas an opcode view of the final byt out = palloca size, offset, id ``` - Like the `alloca` instruction but only used for parameters of internal functions which are passed by memory. +- `calloca` + - ``` + out = calloca size, offset, id, + ``` + - Similar to the `calloca` instruction but only used for parameters of internal functions which are passed by memory. Used at the call-site of a call. - `iload` - ``` out = iload offset diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index bb3fe58a8d..4b777c17fd 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -15,6 +15,7 @@ BranchOptimizationPass, DFTPass, FloatAllocas, + FuncInlinerPass, LoadElimination, LowerDloadPass, MakeSSA, @@ -46,12 +47,10 @@ def generate_assembly_experimental( return compiler.generate_evm(optimize == OptimizationLevel.NONE) -def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: +def _run_passes(fn: IRFunction, optimize: OptimizationLevel, ac: IRAnalysesCache) -> None: # Run passes on Venom IR # TODO: Add support for optimization levels - ac = IRAnalysesCache(fn) - FloatAllocas(ac, fn).run_pass() SimplifyCFGPass(ac, fn).run_pass() @@ -84,8 +83,12 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: BranchOptimizationPass(ac, fn).run_pass() AlgebraicOptimizationPass(ac, fn).run_pass() + + # This improves the performance of cse RemoveUnusedVariablesPass(ac, fn).run_pass() + StoreElimination(ac, fn).run_pass() + RemoveUnusedVariablesPass(ac, fn).run_pass() StoreExpansionPass(ac, fn).run_pass() if optimize == OptimizationLevel.CODESIZE: @@ -94,14 +97,29 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel) -> None: DFTPass(ac, fn).run_pass() -def run_passes_on(ctx: IRContext, optimize: OptimizationLevel): +def _run_global_passes(ctx: IRContext, optimize: OptimizationLevel, ir_analyses: dict) -> None: + FuncInlinerPass(ir_analyses, ctx).run_pass() + + +def run_passes_on(ctx: IRContext, optimize: OptimizationLevel) -> None: + ir_analyses = {} for fn in ctx.functions.values(): - _run_passes(fn, optimize) + ir_analyses[fn] = IRAnalysesCache(fn) + + _run_global_passes(ctx, optimize, ir_analyses) + + ir_analyses = {} + for fn in ctx.functions.values(): + ir_analyses[fn] = IRAnalysesCache(fn) + + for fn in ctx.functions.values(): + _run_passes(fn, optimize, ir_analyses[fn]) def generate_ir(ir: IRnode, optimize: OptimizationLevel) -> IRContext: # Convert "old" IR to "new" IR ctx = ir_node_to_venom(ir) + run_passes_on(ctx, optimize) return ctx diff --git a/vyper/venom/analysis/__init__.py b/vyper/venom/analysis/__init__.py index 4870de3fb7..fd6437b431 100644 --- a/vyper/venom/analysis/__init__.py +++ b/vyper/venom/analysis/__init__.py @@ -2,5 +2,4 @@ from .cfg import CFGAnalysis from .dfg import DFGAnalysis from .dominators import DominatorTreeAnalysis -from .equivalent_vars import VarEquivalenceAnalysis from .liveness import LivenessAnalysis diff --git a/vyper/venom/analysis/analysis.py b/vyper/venom/analysis/analysis.py index 7bff6ba555..4cd573af8d 100644 --- a/vyper/venom/analysis/analysis.py +++ b/vyper/venom/analysis/analysis.py @@ -72,4 +72,8 @@ def force_analysis(self, analysis_cls: Type[IRAnalysis], *args, **kwargs): assert issubclass(analysis_cls, IRAnalysis), f"{analysis_cls} is not an IRAnalysis" if analysis_cls in self.analyses_cache: self.invalidate_analysis(analysis_cls) + + for analysis in self.analyses_cache.values(): + self.request_analysis(analysis.__class__) + return self.request_analysis(analysis_cls, *args, **kwargs) diff --git a/vyper/venom/analysis/cfg.py b/vyper/venom/analysis/cfg.py index 2f90410cd5..77aa2cb362 100644 --- a/vyper/venom/analysis/cfg.py +++ b/vyper/venom/analysis/cfg.py @@ -34,29 +34,48 @@ def analyze(self) -> None: bb.add_cfg_out(next_bb) next_bb.add_cfg_in(bb) - self._compute_dfs_r(self.function.entry) + self._compute_dfs_post_r(self.function.entry) - def _compute_dfs_r(self, bb): + def _compute_dfs_post_r(self, bb): if bb.is_reachable: return bb.is_reachable = True for out_bb in bb.cfg_out: - self._compute_dfs_r(out_bb) + self._compute_dfs_post_r(out_bb) self._dfs.add(bb) @property - def dfs_walk(self) -> Iterator[IRBasicBlock]: + def dfs_pre_walk(self) -> Iterator[IRBasicBlock]: + visited: OrderedSet[IRBasicBlock] = OrderedSet() + + def _visit_dfs_pre_r(bb: IRBasicBlock): + if bb in visited: + return + visited.add(bb) + + yield bb + + for out_bb in bb.cfg_out: + yield from _visit_dfs_pre_r(out_bb) + + yield from _visit_dfs_pre_r(self.function.entry) + + @property + def dfs_post_walk(self) -> Iterator[IRBasicBlock]: return iter(self._dfs) def invalidate(self): from vyper.venom.analysis import DFGAnalysis, DominatorTreeAnalysis, LivenessAnalysis + fn = self.function + for bb in fn.get_basic_blocks(): + bb.cfg_in = OrderedSet() + bb.cfg_out = OrderedSet() + bb.out_vars = OrderedSet() + self.analyses_cache.invalidate_analysis(DominatorTreeAnalysis) self.analyses_cache.invalidate_analysis(LivenessAnalysis) - - self._dfs = None - - # be conservative - assume cfg invalidation invalidates dfg self.analyses_cache.invalidate_analysis(DFGAnalysis) + self._dfs = None diff --git a/vyper/venom/analysis/dfg.py b/vyper/venom/analysis/dfg.py index e528284422..fec8f65e2a 100644 --- a/vyper/venom/analysis/dfg.py +++ b/vyper/venom/analysis/dfg.py @@ -3,7 +3,7 @@ from vyper.utils import OrderedSet from vyper.venom.analysis.analysis import IRAnalysesCache, IRAnalysis from vyper.venom.analysis.liveness import LivenessAnalysis -from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRVariable +from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IROperand, IRVariable from vyper.venom.function import IRFunction @@ -41,6 +41,23 @@ def remove_use(self, op: IRVariable, inst: IRInstruction): uses: OrderedSet = self._dfg_inputs.get(op, OrderedSet()) uses.remove(inst) + def are_equivalent(self, var1: IROperand, var2: IROperand) -> bool: + if var1 == var2: + return True + + if isinstance(var1, IRVariable) and isinstance(var2, IRVariable): + var1 = self._traverse_store_chain(var1) + var2 = self._traverse_store_chain(var2) + + return var1 == var2 + + def _traverse_store_chain(self, var: IRVariable) -> IRVariable: + while True: + inst = self.get_producing_instruction(var) + if inst is None or inst.opcode != "store": + return var + var = inst.operands[0] # type: ignore + @property def outputs(self) -> dict[IRVariable, IRInstruction]: return self._dfg_outputs diff --git a/vyper/venom/analysis/dominators.py b/vyper/venom/analysis/dominators.py index b60f9bdab9..3e60a74213 100644 --- a/vyper/venom/analysis/dominators.py +++ b/vyper/venom/analysis/dominators.py @@ -54,7 +54,7 @@ def _compute_dominators(self): """ Compute dominators """ - basic_blocks = list(self.dfs_order.keys()) + basic_blocks = list(self.dfs_post_order.keys()) self.dominators = {bb: OrderedSet(basic_blocks) for bb in basic_blocks} self.dominators[self.entry_block] = OrderedSet([self.entry_block]) changed = True @@ -80,15 +80,15 @@ def _compute_idoms(self): """ Compute immediate dominators """ - self.immediate_dominators = {bb: None for bb in self.dfs_order.keys()} + self.immediate_dominators = {bb: None for bb in self.dfs_post_order.keys()} self.immediate_dominators[self.entry_block] = self.entry_block - for bb in self.dfs_walk: + for bb in self.dfs_post_walk: if bb == self.entry_block: continue - doms = sorted(self.dominators[bb], key=lambda x: self.dfs_order[x]) + doms = sorted(self.dominators[bb], key=lambda x: self.dfs_post_order[x]) self.immediate_dominators[bb] = doms[1] - self.dominated = {bb: OrderedSet() for bb in self.dfs_walk} + self.dominated = {bb: OrderedSet() for bb in self.dfs_post_walk} for dom, target in self.immediate_dominators.items(): self.dominated[target].add(dom) @@ -96,10 +96,10 @@ def _compute_df(self): """ Compute dominance frontier """ - basic_blocks = self.dfs_walk + basic_blocks = self.dfs_post_walk self.dominator_frontiers = {bb: OrderedSet() for bb in basic_blocks} - for bb in self.dfs_walk: + for bb in self.dfs_post_walk: if len(bb.cfg_in) > 1: for pred in bb.cfg_in: runner = pred @@ -129,12 +129,12 @@ def _intersect(self, bb1, bb2): return bb1 @cached_property - def dfs_walk(self) -> list[IRBasicBlock]: - return list(self.cfg.dfs_walk) + def dfs_post_walk(self) -> list[IRBasicBlock]: + return list(self.cfg.dfs_post_walk) @cached_property - def dfs_order(self) -> dict[IRBasicBlock, int]: - return {bb: idx for idx, bb in enumerate(self.dfs_walk)} + def dfs_post_order(self) -> dict[IRBasicBlock, int]: + return {bb: idx for idx, bb in enumerate(self.dfs_post_walk)} def as_graph(self) -> str: """ diff --git a/vyper/venom/analysis/equivalent_vars.py b/vyper/venom/analysis/equivalent_vars.py index 895895651a..8a333fa401 100644 --- a/vyper/venom/analysis/equivalent_vars.py +++ b/vyper/venom/analysis/equivalent_vars.py @@ -1,40 +1,29 @@ from vyper.venom.analysis import DFGAnalysis, IRAnalysis -from vyper.venom.basicblock import IRVariable class VarEquivalenceAnalysis(IRAnalysis): """ - Generate equivalence sets of variables. This is used to avoid swapping - variables which are the same during venom_to_assembly. Theoretically, - the DFTPass should order variable declarations optimally, but, it is - not aware of the "pickaxe" heuristic in venom_to_assembly, so they can - interfere. + Generate equivalence sets of variables. Essentially, variables chained + by store instructions are equivalent. These are used to avoid swapping + variables which are the same during venom_to_assembly, and are produced + by the StoreExpansionPass. """ def analyze(self): dfg = self.analyses_cache.request_analysis(DFGAnalysis) - equivalence_set: dict[IRVariable, int] = {} + self._equivalence_set = {} - for bag, (var, inst) in enumerate(dfg._dfg_outputs.items()): + for output, inst in dfg.outputs.items(): if inst.opcode != "store": continue - source = inst.operands[0] + self._equivalence_set[output] = self._get_equivalent(inst.operands[0]) - assert var not in equivalence_set # invariant - if source in equivalence_set: - equivalence_set[var] = equivalence_set[source] - continue - else: - equivalence_set[var] = bag - equivalence_set[source] = bag - - self._equivalence_set = equivalence_set + def _get_equivalent(self, var): + if var in self._equivalence_set: + return self._equivalence_set[var] + return var def equivalent(self, var1, var2): - if var1 not in self._equivalence_set: - return False - if var2 not in self._equivalence_set: - return False - return self._equivalence_set[var1] == self._equivalence_set[var2] + return self._get_equivalent(var1) == self._get_equivalent(var2) diff --git a/vyper/venom/analysis/fcg.py b/vyper/venom/analysis/fcg.py new file mode 100644 index 0000000000..98e2d51914 --- /dev/null +++ b/vyper/venom/analysis/fcg.py @@ -0,0 +1,49 @@ +from vyper.utils import OrderedSet +from vyper.venom.analysis.analysis import IRAnalysesCache, IRAnalysis +from vyper.venom.basicblock import IRInstruction, IRLabel +from vyper.venom.context import IRContext +from vyper.venom.function import IRFunction + + +class FCGAnalysis(IRAnalysis): + """ + Compute the function call graph for the context. + """ + + ctx: IRContext + call_sites: dict[IRFunction, OrderedSet[IRInstruction]] + callees: dict[IRFunction, OrderedSet[IRFunction]] + + def __init__(self, analyses_cache: IRAnalysesCache, function: IRFunction): + super().__init__(analyses_cache, function) + self.ctx = function.ctx + self.call_sites = dict() + self.callees = dict() + + def analyze(self) -> None: + ctx = self.ctx + for func in ctx.get_functions(): + self.call_sites[func] = OrderedSet() + self.callees[func] = OrderedSet() + + for fn in ctx.get_functions(): + self._analyze_function(fn) + + def get_call_sites(self, fn: IRFunction) -> OrderedSet[IRInstruction]: + return self.call_sites.get(fn, OrderedSet()) + + def get_callees(self, fn: IRFunction) -> OrderedSet[IRFunction]: + return self.callees[fn] + + def _analyze_function(self, fn: IRFunction) -> None: + for bb in fn.get_basic_blocks(): + for inst in bb.instructions: + if inst.opcode == "invoke": + label = inst.operands[0] + assert isinstance(label, IRLabel) # mypy help + callee = self.ctx.get_function(label) + self.callees[fn].add(callee) + self.call_sites[callee].add(inst) + + def invalidate(self): + pass diff --git a/vyper/venom/analysis/liveness.py b/vyper/venom/analysis/liveness.py index 0ccda3de2c..e97f6fea03 100644 --- a/vyper/venom/analysis/liveness.py +++ b/vyper/venom/analysis/liveness.py @@ -15,7 +15,7 @@ def analyze(self): cfg = self.analyses_cache.request_analysis(CFGAnalysis) self._reset_liveness() - worklist = deque(cfg.dfs_walk) + worklist = deque(cfg.dfs_post_walk) while len(worklist) > 0: changed = False diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index 0cdf849d51..5a22730a1e 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -1,5 +1,6 @@ import json import re +from contextvars import ContextVar from typing import TYPE_CHECKING, Any, Iterator, Optional, Union import vyper.venom.effects as effects @@ -98,6 +99,8 @@ if TYPE_CHECKING: from vyper.venom.function import IRFunction +ir_printer = ContextVar("ir_printer", default=None) + def flip_comparison_opcode(opcode): if opcode in ("gt", "sgt"): @@ -387,6 +390,12 @@ def remove_phi_operand(self, label: IRLabel) -> None: del self.operands[i : i + 2] return + @property + def code_size_cost(self) -> int: + if self.opcode == "store": + return 1 + return 2 + def get_ast_source(self) -> Optional[IRnode]: if self.ast_source: return self.ast_source @@ -396,6 +405,40 @@ def get_ast_source(self) -> Optional[IRnode]: return inst.ast_source return self.parent.parent.ast_source + def copy(self, prefix: str = "") -> "IRInstruction": + ops: list[IROperand] = [] + for op in self.operands: + if isinstance(op, IRLabel): + ops.append(IRLabel(op.value)) + elif isinstance(op, IRVariable): + ops.append(IRVariable(f"{prefix}{op.name}")) + else: + ops.append(IRLiteral(op.value)) + + output = None + if self.output: + output = IRVariable(f"{prefix}{self.output.name}") + + inst = IRInstruction(self.opcode, ops, output) + inst.parent = self.parent + inst.liveness = self.liveness.copy() + inst.annotation = self.annotation + inst.ast_source = self.ast_source + inst.error_msg = self.error_msg + return inst + + def str_short(self) -> str: + s = "" + if self.output: + s += f"{self.output} = " + opcode = f"{self.opcode} " if self.opcode != "store" else "" + s += opcode + operands = self.operands + if opcode not in ["jmp", "jnz", "invoke"]: + operands = list(reversed(operands)) + s += ", ".join([(f"@{op}" if isinstance(op, IRLabel) else str(op)) for op in operands]) + return s + def __repr__(self) -> str: s = "" if self.output: @@ -407,7 +450,6 @@ def __repr__(self) -> str: operands = [operands[0]] + list(reversed(operands[1:])) elif self.opcode not in ("jmp", "jnz", "phi"): operands = reversed(operands) # type: ignore - s += ", ".join([(f"@{op}" if isinstance(op, IRLabel) else str(op)) for op in operands]) if self.annotation: @@ -559,6 +601,30 @@ def remove_instruction(self, instruction: IRInstruction) -> None: assert isinstance(instruction, IRInstruction), "instruction must be an IRInstruction" self.instructions.remove(instruction) + def remove_instructions_after(self, instruction: IRInstruction) -> None: + assert isinstance(instruction, IRInstruction), "instruction must be an IRInstruction" + assert instruction in self.instructions, "instruction must be in basic block" + # TODO: make sure this has coverage in the test suite + self.instructions = self.instructions[: self.instructions.index(instruction) + 1] + + def ensure_well_formed(self): + for inst in self.instructions: + if inst.opcode == "revert": + self.remove_instructions_after(inst) + self.append_instruction("stop") # TODO: make revert a bb terminator? + break + # TODO: remove once clear_dead_instructions is removed + self.clear_dead_instructions() + + def key(inst): + if inst.opcode in ("phi", "param"): + return 0 + if inst.is_bb_terminator: + return 2 + return 1 + + self.instructions.sort(key=key) + @property def phi_instructions(self) -> Iterator[IRInstruction]: for inst in self.instructions: @@ -587,6 +653,10 @@ def pseudo_instructions(self) -> Iterator[IRInstruction]: def body_instructions(self) -> Iterator[IRInstruction]: return (inst for inst in self.instructions[:-1] if not inst.is_pseudo) + @property + def code_size_cost(self) -> int: + return sum(inst.code_size_cost for inst in self.instructions) + def replace_operands(self, replacements: dict) -> None: """ Update operands with replacements. @@ -665,21 +735,39 @@ def liveness_in_vars(self) -> OrderedSet[IRVariable]: return inst.liveness return OrderedSet() - def copy(self): - bb = IRBasicBlock(self.label, self.parent) - bb.instructions = self.instructions.copy() - bb.cfg_in = self.cfg_in.copy() - bb.cfg_out = self.cfg_out.copy() - bb.out_vars = self.out_vars.copy() + def copy(self, prefix: str = "") -> "IRBasicBlock": + new_label = IRLabel(f"{prefix}{self.label.value}") + bb = IRBasicBlock(new_label, self.parent) + bb.instructions = [inst.copy(prefix) for inst in self.instructions] + for inst in bb.instructions: + inst.parent = bb + bb.cfg_in = OrderedSet() + bb.cfg_out = OrderedSet() + bb.out_vars = OrderedSet() return bb def __repr__(self) -> str: - s = f"{self.label}: ; IN={[bb.label for bb in self.cfg_in]}" - s += f" OUT={[bb.label for bb in self.cfg_out]} => {self.out_vars}\n" - for instruction in self.instructions: - s += f" {str(instruction).strip()}\n" - if len(self.instructions) > 30: - s += f" ; {self.label}\n" - if len(self.instructions) > 30 or self.parent.num_basic_blocks > 5: - s += f" ; ({self.parent.name})\n\n" + printer = ir_printer.get() + + s = ( + f"{repr(self.label)}: ; IN={[bb.label for bb in self.cfg_in]}" + f" OUT={[bb.label for bb in self.cfg_out]} => {self.out_vars}\n" + ) + if printer and hasattr(printer, "_pre_block"): + s += printer._pre_block(self) + for inst in self.instructions: + if printer and hasattr(printer, "_pre_instruction"): + s += printer._pre_instruction(inst) + s += f" {str(inst).strip()}" + if printer and hasattr(printer, "_post_instruction"): + s += printer._post_instruction(inst) + s += "\n" return s + + +class IRPrinter: + def _pre_instruction(self, inst: IRInstruction) -> str: + return "" + + def _post_instruction(self, inst: IRInstruction) -> str: + return "" diff --git a/vyper/venom/context.py b/vyper/venom/context.py index 0c5cbc379c..ce08125b74 100644 --- a/vyper/venom/context.py +++ b/vyper/venom/context.py @@ -1,8 +1,8 @@ import textwrap from dataclasses import dataclass, field -from typing import Optional +from typing import Iterator, Optional -from vyper.venom.basicblock import IRLabel +from vyper.venom.basicblock import IRBasicBlock, IRLabel, IRVariable from vyper.venom.function import IRFunction @@ -32,25 +32,40 @@ def __str__(self): class IRContext: functions: dict[IRLabel, IRFunction] + entry_function: Optional[IRFunction] ctor_mem_size: Optional[int] immutables_len: Optional[int] data_segment: list[DataSection] last_label: int + last_variable: int def __init__(self) -> None: self.functions = {} + self.entry_function = None self.ctor_mem_size = None self.immutables_len = None self.data_segment = [] self.last_label = 0 + self.last_variable = 0 + + def get_basic_blocks(self) -> Iterator[IRBasicBlock]: + for fn in self.functions.values(): + for bb in fn.get_basic_blocks(): + yield bb def add_function(self, fn: IRFunction) -> None: fn.ctx = self self.functions[fn.name] = fn + def remove_function(self, fn: IRFunction) -> None: + del self.functions[fn.name] + def create_function(self, name: str) -> IRFunction: label = IRLabel(name, True) + if label in self.functions: + return self.functions[label] fn = IRFunction(label, self) + fn.append_basic_block(IRBasicBlock(label, fn)) self.add_function(fn) return fn @@ -59,12 +74,22 @@ def get_function(self, name: IRLabel) -> IRFunction: return self.functions[name] raise Exception(f"Function {name} not found in context") + def get_functions(self) -> Iterator[IRFunction]: + return iter(self.functions.values()) + def get_next_label(self, suffix: str = "") -> IRLabel: if suffix != "": suffix = f"_{suffix}" self.last_label += 1 return IRLabel(f"{self.last_label}{suffix}") + def get_next_variable(self) -> IRVariable: + self.last_variable += 1 + return IRVariable(f"%{self.last_variable}") + + def get_last_variable(self) -> str: + return f"%{self.last_variable}" + def chain_basic_blocks(self) -> None: """ Chain basic blocks together. This is necessary for the IR to be valid, and is done after diff --git a/vyper/venom/function.py b/vyper/venom/function.py index f02da77fe3..6069b9ec98 100644 --- a/vyper/venom/function.py +++ b/vyper/venom/function.py @@ -1,10 +1,22 @@ import textwrap +from dataclasses import dataclass from typing import Iterator, Optional from vyper.codegen.ir_node import IRnode from vyper.venom.basicblock import IRBasicBlock, IRLabel, IRVariable +@dataclass +class IRParameter: + name: str + index: int + offset: int + size: int + call_site_var: Optional[IRVariable] + func_var: Optional[IRVariable] + addr_var: Optional[IRVariable] + + class IRFunction: """ Function that contains basic blocks. @@ -42,13 +54,16 @@ def append_basic_block(self, bb: IRBasicBlock): Append basic block to function. """ assert isinstance(bb, IRBasicBlock), bb - assert bb.label.name not in self._basic_block_dict, bb.label + # assert bb.label.name not in self._basic_block_dict, bb.label self._basic_block_dict[bb.label.name] = bb def remove_basic_block(self, bb: IRBasicBlock): assert isinstance(bb, IRBasicBlock), bb del self._basic_block_dict[bb.label.name] + def has_basic_block(self, label: str) -> bool: + return label in self._basic_block_dict + def get_basic_block(self, label: Optional[str] = None) -> IRBasicBlock: """ Get basic block by label. @@ -72,6 +87,10 @@ def get_basic_blocks(self) -> Iterator[IRBasicBlock]: def num_basic_blocks(self) -> int: return len(self._basic_block_dict) + @property + def code_size_cost(self) -> int: + return sum(bb.code_size_cost for bb in self.get_basic_blocks()) + def get_terminal_basicblocks(self) -> Iterator[IRBasicBlock]: """ Get basic blocks that are terminal. @@ -149,6 +168,20 @@ def pop_source(self): assert len(self._error_msg_stack) > 0, "Empty error stack" self._error_msg_stack.pop() + def get_param_at_offset(self, offset: int) -> Optional[IRParameter]: + for param in self.args: + if param.offset == offset: + return param + return None + + def get_param_by_name(self, var: IRVariable | str) -> Optional[IRParameter]: + if isinstance(var, str): + var = IRVariable(var) + for param in self.args: + if f"%{param.name}" == var.name: + return param + return None + @property def ast_source(self) -> Optional[IRnode]: return self._ast_source_stack[-1] if len(self._ast_source_stack) > 0 else None @@ -179,10 +212,12 @@ def chain_basic_blocks(self) -> None: else: bb.append_instruction("stop") - def copy(self): - new = IRFunction(self.name) - new._basic_block_dict = self._basic_block_dict.copy() - new.last_variable = self.last_variable + def copy(self, prefix: str = ""): + new_label = IRLabel(f"{prefix}{self.name.value}") + new = IRFunction(new_label) + for bb in self.get_basic_blocks(): + new_bb = bb.copy(prefix) + new.append_basic_block(new_bb) return new def as_graph(self, only_subgraph=False) -> str: diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index f46457b77f..4e772317ca 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -1,7 +1,9 @@ import functools import re +from collections import defaultdict from typing import Optional +from vyper.codegen.context import Alloca from vyper.codegen.ir_node import IRnode from vyper.evm.opcodes import get_opcodes from vyper.venom.basicblock import ( @@ -63,7 +65,7 @@ "gasprice", "gaslimit", "returndatasize", - "mload", + # "mload", "iload", "istore", "dload", @@ -107,8 +109,10 @@ NOOP_INSTRUCTIONS = frozenset(["pass", "cleanup_repeat", "var_list", "unique_symbol"]) -SymbolTable = dict[str, Optional[IROperand]] -_alloca_table: SymbolTable = None # type: ignore +SymbolTable = dict[str, IROperand] +_alloca_table: dict[int, IROperand] +# assumption: callsites (return pc labels) are globally unique. +_callsites: dict[str, list[Alloca]] MAIN_ENTRY_LABEL_NAME = "__main_entry" @@ -116,16 +120,22 @@ def ir_node_to_venom(ir: IRnode) -> IRContext: _ = ir.unique_symbols # run unique symbols check - global _alloca_table + global _alloca_table, _callsites _alloca_table = {} + _callsites = defaultdict(list) ctx = IRContext() fn = ctx.create_function(MAIN_ENTRY_LABEL_NAME) + ctx.entry_function = fn _convert_ir_bb(fn, ir, {}) ctx.chain_basic_blocks() + for fn in ctx.functions.values(): + for bb in fn.get_basic_blocks(): + bb.ensure_well_formed() + return ctx @@ -154,20 +164,30 @@ def _append_return_args(fn: IRFunction, ofst: int = 0, size: int = 0): bb.append_instruction("store", size, ret=ret_size) -def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optional[IRVariable]: +def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optional[IROperand]: + global _callsites setup_ir = ir.args[1] goto_ir = [ir for ir in ir.args if ir.value == "goto"][0] target_label = goto_ir.args[0].value # goto - return_buf_ir = goto_ir.args[1] # return buffer ret_args: list[IROperand] = [IRLabel(target_label)] # type: ignore + func_t = ir.passthrough_metadata["func_t"] + assert func_t is not None, "func_t not found in passthrough metadata" if setup_ir != goto_ir: _convert_ir_bb(fn, setup_ir, symbols) - return_buf = _convert_ir_bb(fn, return_buf_ir, symbols) + converted_args = _convert_ir_bb_list(fn, goto_ir.args[1:], symbols) + + callsite_op = converted_args[-1] + assert isinstance(callsite_op, IRLabel), converted_args bb = fn.get_basic_block() - if len(goto_ir.args) > 2: + return_buf = None + + if len(converted_args) > 1: + return_buf = converted_args[0] + + if return_buf is not None: ret_args.append(return_buf) # type: ignore bb.append_invoke_instruction(ret_args, returns=False) # type: ignore @@ -175,23 +195,59 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio return return_buf +_current_func_t = None +_current_context = None + + +def _is_word_type(typ): + return typ._is_prim_word + # return typ.memory_bytes_required == 32 + + +# func_t: ContractFunctionT +def _returns_word(func_t) -> bool: + return_t = func_t.return_type + return return_t is not None and _is_word_type(return_t) + + def _handle_internal_func( - fn: IRFunction, ir: IRnode, does_return_data: bool, symbols: SymbolTable + # TODO: remove does_return_data, replace with `func_t.return_type is not None` + fn: IRFunction, + ir: IRnode, + does_return_data: bool, + symbols: SymbolTable, ) -> IRFunction: + global _alloca_table, _current_func_t, _current_context + + _current_func_t = ir.passthrough_metadata["func_t"] + _current_context = ir.passthrough_metadata["context"] + fn = fn.ctx.create_function(ir.args[0].args[0].value) + bb = fn.get_basic_block() + _saved_alloca_table = _alloca_table + _alloca_table = {} + # return buffer if does_return_data: - symbols["return_buffer"] = bb.append_instruction("param") + buf = bb.append_instruction("param") bb.instructions[-1].annotation = "return_buffer" + assert buf is not None # help mypy + symbols["return_buffer"] = buf + # return address - symbols["return_pc"] = bb.append_instruction("param") + return_pc = bb.append_instruction("param") + assert return_pc is not None # help mypy + symbols["return_pc"] = return_pc + bb.instructions[-1].annotation = "return_pc" _convert_ir_bb(fn, ir.args[0].args[2], symbols) + _alloca_table = _saved_alloca_table + return fn @@ -386,6 +442,9 @@ def _convert_ir_bb(fn, ir, symbols): code = ir.args[2] _convert_ir_bb(fn, code, symbols) elif ir.value == "exit_to": + # TODO: cleanup + global _current_func_t + args = _convert_ir_bb_list(fn, ir.args[1:], symbols) var_list = args _append_return_args(fn, *var_list) @@ -398,6 +457,7 @@ def _convert_ir_bb(fn, ir, symbols): label = IRLabel(ir.args[0].value) if label.value == "return_pc": label = symbols.get("return_pc") + # return label should be top of stack bb.append_instruction("ret", label) else: bb.append_instruction("jmp", label) @@ -408,7 +468,11 @@ def _convert_ir_bb(fn, ir, symbols): val, ptr = _convert_ir_bb_list(fn, reversed(ir.args), symbols) return fn.get_basic_block().append_instruction("mstore", val, ptr) + elif ir.value == "mload": + arg = ir.args[0] + ptr = _convert_ir_bb(fn, arg, symbols) + return fn.get_basic_block().append_instruction("mload", ptr) elif ir.value == "ceil32": x = ir.args[0] expanded = IRnode.from_list(["and", ["add", x, 31], ["not", 31]]) @@ -525,6 +589,21 @@ def emit_body_blocks(): _alloca_table[alloca._id] = ptr return _alloca_table[alloca._id] + elif ir.value.startswith("$calloca"): + global _callsites + alloca = ir.passthrough_metadata["alloca"] + assert alloca._callsite is not None + if alloca._id not in _alloca_table: + bb = fn.get_basic_block() + ptr = IRLiteral(alloca.offset) + + _alloca_table[alloca._id] = ptr + ret = _alloca_table[alloca._id] + # assumption: callocas appear in the same order as the + # order of arguments to the function. + _callsites[alloca._callsite].append(alloca) + return ret + return symbols.get(ir.value) elif ir.is_literal: return IRLiteral(ir.value) diff --git a/vyper/venom/parser.py b/vyper/venom/parser.py index 5ccc29b7a4..2b47b2e335 100644 --- a/vyper/venom/parser.py +++ b/vyper/venom/parser.py @@ -49,7 +49,7 @@ CONST: SIGNED_INT OPCODE: CNAME - VAR_IDENT: "%" (DIGIT|LETTER|"_"|":")+ + VAR_IDENT: "%" (DIGIT|LETTER|"_"|":"|"%"|"@")+ # handy for identifier to be an escaped string sometimes # (especially for machine-generated labels) @@ -124,6 +124,8 @@ def start(self, children) -> IRContext: funcs = children for fn_name, blocks in funcs: fn = ctx.create_function(fn_name) + if ctx.entry_function is None: + ctx.entry_function = fn fn._basic_block_dict.clear() for block_name, instructions in blocks: diff --git a/vyper/venom/passes/__init__.py b/vyper/venom/passes/__init__.py index a3227dcf4b..489519b89b 100644 --- a/vyper/venom/passes/__init__.py +++ b/vyper/venom/passes/__init__.py @@ -2,6 +2,7 @@ from .branch_optimization import BranchOptimizationPass from .dft import DFTPass from .float_allocas import FloatAllocas +from .func_inliner import FuncInlinerPass from .literals_codesize import ReduceLiteralsCodesize from .load_elimination import LoadElimination from .lower_dload import LowerDloadPass diff --git a/vyper/venom/passes/base_pass.py b/vyper/venom/passes/base_pass.py index 3951ac4455..2999d3a4b1 100644 --- a/vyper/venom/passes/base_pass.py +++ b/vyper/venom/passes/base_pass.py @@ -1,4 +1,8 @@ +from typing import Optional + +from vyper.compiler.settings import Settings, get_global_settings from vyper.venom.analysis import IRAnalysesCache +from vyper.venom.context import IRContext from vyper.venom.function import IRFunction @@ -16,3 +20,27 @@ def __init__(self, analyses_cache: IRAnalysesCache, function: IRFunction): def run_pass(self, *args, **kwargs): raise NotImplementedError(f"Not implemented! {self.__class__}.run_pass()") + + +class IRGlobalPass: + """ + Base class for all Venom IR passes. + """ + + ctx: IRContext + analyses_caches: dict[IRFunction, IRAnalysesCache] + settings: Settings + + def __init__( + self, + analyses_caches: dict[IRFunction, IRAnalysesCache], + ctx: IRContext, + settings: Optional[Settings] = None, + ): + self.analyses_caches = analyses_caches + self.ctx = ctx + settings = settings or get_global_settings() + self.settings = settings or Settings() + + def run_pass(self, *args, **kwargs): + raise NotImplementedError(f"Not implemented! {self.__class__}.run_pass()") diff --git a/vyper/venom/passes/float_allocas.py b/vyper/venom/passes/float_allocas.py index 81fa115645..794c1e497d 100644 --- a/vyper/venom/passes/float_allocas.py +++ b/vyper/venom/passes/float_allocas.py @@ -23,7 +23,7 @@ def run_pass(self): # Extract alloca instructions non_alloca_instructions = [] for inst in bb.instructions: - if inst.opcode in ("alloca", "palloca"): + if inst.opcode in ("alloca", "palloca", "calloca"): # note: order of allocas impacts bytecode. # TODO: investigate. entry_bb.insert_instruction(inst) diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py new file mode 100644 index 0000000000..639ee9ff01 --- /dev/null +++ b/vyper/venom/passes/func_inliner.py @@ -0,0 +1,184 @@ +from typing import List, Optional + +from vyper.compiler.settings import OptimizationLevel +from vyper.exceptions import CompilerPanic +from vyper.utils import OrderedSet +from vyper.venom.analysis.cfg import CFGAnalysis +from vyper.venom.analysis.dfg import DFGAnalysis +from vyper.venom.analysis.fcg import FCGAnalysis +from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRLabel +from vyper.venom.function import IRFunction +from vyper.venom.passes import FloatAllocas +from vyper.venom.passes.base_pass import IRGlobalPass + + +class FuncInlinerPass(IRGlobalPass): + """ + This pass inlines functions into their call sites to reduce function call overhead. + + Limitations: + - Does not handle recursive functions + + Side effects: + - Modifies the control flow graph + - Invalidates DFG, CFG and VarEquivalence analyses + """ + + _RETURN_BUFFER_ANNOTATION = "return_buffer" + _RETURN_PC_ANNOTATION = "return_pc" + _RETURN_OFFSET_MARKER = "ret_ofst" + _RETURN_SIZE_MARKER = "ret_size" + + inline_count: int + fcg: FCGAnalysis + + def run_pass(self): + entry = self.ctx.entry_function + self.inline_count = 0 + + function_count = len(self.ctx.functions) + self.fcg = self.analyses_caches[entry].request_analysis(FCGAnalysis) + self.walk = self._build_call_walk(entry) + + for _ in range(function_count): + candidate = self._select_inline_candidate() + if candidate is None: + return + + # print(f"Inlining function {candidate.name} with cost {candidate.code_size_cost}") + + calls = self.fcg.get_call_sites(candidate) + self._inline_function(candidate, calls) + self.ctx.remove_function(candidate) + self.walk.remove(candidate) + + self.fcg = self.analyses_caches[entry].force_analysis(FCGAnalysis) + + def _select_inline_candidate(self) -> Optional[IRFunction]: + for func in self.walk: + call_count = len(self.fcg.get_call_sites(func)) + if call_count == 0: + continue + + # Always inline if there is only one call site. + if call_count == 1: + return func + + # Decide whether to inline based on the optimization level. + if self.settings.optimize == OptimizationLevel.CODESIZE: + continue + elif self.settings.optimize == OptimizationLevel.GAS: + if func.code_size_cost <= 15: + return func + elif self.settings.optimize == OptimizationLevel.NONE: + continue + else: + raise CompilerPanic( + f"Unsupported inlining optimization level: {self.settings.optimize}" + ) + + return None + + def _inline_function(self, func: IRFunction, call_sites: List[IRInstruction]) -> None: + """ + Inline function into call sites. + """ + for call_site in call_sites: + FloatAllocas(self.analyses_caches[func], func).run_pass() + self._inline_call_site(func, call_site) + fn = call_site.parent.parent + self.analyses_caches[fn].invalidate_analysis(DFGAnalysis) + self.analyses_caches[fn].invalidate_analysis(CFGAnalysis) + + def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: + """ + Inline function into call site. + """ + # TODO: not allowed at all in Vyper at the moment + # but we could support it if we want to with Venom. + # (I think we should support tail call optimizable cases at least) + # if func == call_site.parent.parent: + # raise CompilerPanic("Recursive function inlining is not supported") + + if call_site.opcode != "invoke": + raise CompilerPanic(f"Expected invoke instruction, got {call_site.opcode}") + + prefix = f"inline_{self.inline_count}_" + self.inline_count += 1 + call_site_bb = call_site.parent + call_site_func = call_site_bb.parent + + call_site_return = IRBasicBlock( + self.ctx.get_next_label(f"{prefix}inline_return"), call_site_bb.parent + ) + call_idx = call_site_bb.instructions.index(call_site) + + for inst in call_site_bb.instructions[call_idx + 1 :]: + call_site_return.insert_instruction(inst) + call_site_func.append_basic_block(call_site_return) + + func_copy = func.copy(prefix) + + for bb in func_copy.get_basic_blocks(): + bb.parent = call_site_func + call_site_func.append_basic_block(bb) + for inst in bb.instructions: + if inst.opcode == "param": + if inst.annotation == self._RETURN_BUFFER_ANNOTATION: + inst.opcode = "store" + inst.operands = [call_site.operands[1]] + elif inst.annotation == self._RETURN_PC_ANNOTATION: + inst.make_nop() + else: + assert inst.annotation is not None + arg = func.get_param_by_name(inst.annotation) + assert arg is not None + inst.opcode = "store" + inst.operands = [call_site.operands[arg.index + 1]] + inst.annotation = None + elif inst.opcode == "palloca": + inst.opcode = "store" + inst.operands = [inst.operands[0]] + elif inst.opcode == "store": + assert inst.output is not None # mypy is not smart enough + if ( + self._RETURN_OFFSET_MARKER in inst.output.name + or self._RETURN_SIZE_MARKER in inst.output.name + ): + inst.make_nop() + elif inst.opcode == "ret": + inst.opcode = "jmp" + inst.operands = [call_site_return.label] + elif inst.opcode in ["jmp", "jnz", "djmp", "phi"]: + for i, label in enumerate(inst.operands): + if isinstance(label, IRLabel) and func.has_basic_block(label.name): + inst.operands[i] = IRLabel(f"{prefix}{label.name}") + elif inst.opcode == "revert": + bb.remove_instructions_after(inst) + bb.append_instruction("stop") + break + + call_site_bb.instructions = call_site_bb.instructions[:call_idx] + call_site_bb.append_instruction("jmp", func_copy.entry.label) + + def _build_call_walk(self, function: IRFunction) -> OrderedSet[IRFunction]: + """ + DFS walk over the call graph. + """ + visited = set() + call_walk = [] + + def dfs(fn): + if fn in visited: + return + visited.add(fn) + + called_functions = self.fcg.get_callees(fn) + for func in called_functions: + dfs(func) + + call_walk.append(fn) + + dfs(function) + + return OrderedSet(call_walk) diff --git a/vyper/venom/passes/load_elimination.py b/vyper/venom/passes/load_elimination.py index f805f4c091..685896053c 100644 --- a/vyper/venom/passes/load_elimination.py +++ b/vyper/venom/passes/load_elimination.py @@ -1,6 +1,6 @@ from typing import Optional -from vyper.venom.analysis import DFGAnalysis, LivenessAnalysis, VarEquivalenceAnalysis +from vyper.venom.analysis import DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRLiteral from vyper.venom.effects import Effects from vyper.venom.passes.base_pass import IRPass @@ -24,6 +24,8 @@ class LoadElimination(IRPass): # should this be renamed to EffectsElimination? def run_pass(self): + self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) + for bb in self.function.get_basic_blocks(): self._process_bb(bb, Effects.MEMORY, "mload", "mstore") self._process_bb(bb, Effects.TRANSIENT, "tload", "tstore") @@ -33,10 +35,9 @@ def run_pass(self): self.analyses_cache.invalidate_analysis(LivenessAnalysis) self.analyses_cache.invalidate_analysis(DFGAnalysis) - self.analyses_cache.invalidate_analysis(VarEquivalenceAnalysis) def equivalent(self, op1, op2): - return op1 == op2 + return op1 == op2 or self.dfg.are_equivalent(op1, op2) def get_literal(self, op): if isinstance(op, IRLiteral): diff --git a/vyper/venom/passes/make_ssa.py b/vyper/venom/passes/make_ssa.py index ee013e0f1d..043160d0a2 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 = {bb: 0 for bb in self.dom.dfs_walk} - has_already = {bb: 0 for bb in self.dom.dfs_walk} + work = {var: 0 for var in self.dom.dfs_post_walk} + has_already = {var: 0 for var in self.dom.dfs_post_walk} i = 0 # Iterate over all variables @@ -148,7 +148,7 @@ def _compute_defs(self): Compute the definition points of variables in the function. """ self.defs = {} - for bb in self.dom.dfs_walk: + for bb in self.dom.dfs_post_walk: assignments = bb.get_assignments() for var in assignments: if var not in self.defs: diff --git a/vyper/venom/passes/mem2var.py b/vyper/venom/passes/mem2var.py index 9f985e2b0b..8b1fb1f0f8 100644 --- a/vyper/venom/passes/mem2var.py +++ b/vyper/venom/passes/mem2var.py @@ -1,3 +1,4 @@ +from vyper.utils import all_nonempty from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRInstruction, IRVariable from vyper.venom.function import IRFunction @@ -19,30 +20,31 @@ def run_pass(self): self.var_name_count = 0 for var, inst in dfg.outputs.items(): if inst.opcode == "alloca": - self._process_alloca_var(dfg, var) + self._process_alloca_var(dfg, inst, var) elif inst.opcode == "palloca": self._process_palloca_var(dfg, inst, var) self.analyses_cache.invalidate_analysis(DFGAnalysis) self.analyses_cache.invalidate_analysis(LivenessAnalysis) - def _mk_varname(self, varname: str): + def _mk_varname(self, varname: str, alloca_id: int): varname = varname.removeprefix("%") - varname = f"var{varname}_{self.var_name_count}" + varname = f"alloca_{alloca_id}_@{varname}_{self.var_name_count}" self.var_name_count += 1 return varname - def _process_alloca_var(self, dfg: DFGAnalysis, var: IRVariable): + def _process_alloca_var(self, dfg: DFGAnalysis, alloca_inst, 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. """ uses = dfg.get_uses(var) - if not all([inst.opcode in ["mstore", "mload", "return"] for inst in uses]): + if not all_nonempty(inst.opcode in ["mstore", "mload", "return"] for inst in uses): return - var_name = self._mk_varname(var.name) + alloca_id = alloca_inst.operands[2] + var_name = self._mk_varname(var.value, alloca_id.value) var = IRVariable(var_name) for inst in uses: if inst.opcode == "mstore": @@ -65,15 +67,16 @@ def _process_palloca_var(self, dfg: DFGAnalysis, palloca_inst: IRInstruction, va instructions, it is promoted to a stack variable. Otherwise, it is left as is. """ uses = dfg.get_uses(var) - if not all(inst.opcode in ["mstore", "mload"] for inst in uses): + if not all_nonempty(inst.opcode in ["mstore", "mload"] for inst in uses): return - var_name = self._mk_varname(var.name) + ofst, _size, alloca_id = palloca_inst.operands + var_name = self._mk_varname(var.value, alloca_id.value) 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.operands = [ofst] palloca_inst.output = var for inst in uses: diff --git a/vyper/venom/passes/sccp/sccp.py b/vyper/venom/passes/sccp/sccp.py index dbe5e607da..e4f9633d9f 100644 --- a/vyper/venom/passes/sccp/sccp.py +++ b/vyper/venom/passes/sccp/sccp.py @@ -61,11 +61,12 @@ def __init__(self, analyses_cache: IRAnalysesCache, function: IRFunction): super().__init__(analyses_cache, function) self.lattice = {} self.work_list: list[WorkListItem] = [] - self.cfg_dirty = False def run_pass(self): self.fn = self.function self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) # type: ignore + self.analyses_cache.request_analysis(CFGAnalysis) + self.cfg_dirty = False self._calculate_sccp(self.fn.entry) self._propagate_constants() @@ -177,8 +178,8 @@ def _visit_phi(self, inst: IRInstruction): def _visit_expr(self, inst: IRInstruction): opcode = inst.opcode - if opcode in ["store", "alloca", "palloca"]: - assert inst.output is not None, "Got store/alloca without output" + if opcode in ("store", "alloca", "palloca", "calloca"): + assert inst.output is not None, inst out = self._eval_from_lattice(inst.operands[0]) self._set_lattice(inst.output, out) self._add_ssa_work_items(inst) diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index 37d2a58314..f49d97fd3f 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -10,12 +10,7 @@ optimize_assembly, ) from vyper.utils import MemoryPositions, OrderedSet, wrap256 -from vyper.venom.analysis import ( - CFGAnalysis, - IRAnalysesCache, - LivenessAnalysis, - VarEquivalenceAnalysis, -) +from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, IRAnalysesCache, LivenessAnalysis from vyper.venom.basicblock import ( TEST_INSTRUCTIONS, IRBasicBlock, @@ -142,6 +137,7 @@ class VenomCompiler: visited_instructions: OrderedSet # {IRInstruction} visited_basicblocks: OrderedSet # {IRBasicBlock} liveness_analysis: LivenessAnalysis + dfg: DFGAnalysis def __init__(self, ctxs: list[IRContext]): self.ctxs = ctxs @@ -163,7 +159,7 @@ def generate_evm(self, no_optimize: bool = False) -> list[str]: NormalizationPass(ac, fn).run_pass() self.liveness_analysis = ac.request_analysis(LivenessAnalysis) - self.equivalence = ac.request_analysis(VarEquivalenceAnalysis) + self.dfg = ac.request_analysis(DFGAnalysis) ac.request_analysis(CFGAnalysis) assert fn.normalized, "Non-normalized CFG!" @@ -232,7 +228,7 @@ def _stack_reorder( continue to_swap = stack.peek(final_stack_depth) - if self.equivalence.equivalent(op, to_swap): + if self.dfg.are_equivalent(op, to_swap): # perform a "virtual" swap stack.poke(final_stack_depth, op) stack.poke(depth, to_swap) @@ -371,8 +367,9 @@ def _generate_evm_for_instruction( if opcode in ["jmp", "djmp", "jnz", "invoke"]: operands = list(inst.get_non_label_operands()) - elif opcode in ("alloca", "palloca"): - offset, _size = inst.operands + elif opcode in ("alloca", "palloca", "calloca"): + assert len(inst.operands) == 3, f"alloca/palloca must have 3 operands, got {inst}" + offset, _size, _id = inst.operands operands = [offset] # iload and istore are special cases because they can take a literal @@ -473,7 +470,7 @@ def _generate_evm_for_instruction( # Step 5: Emit the EVM instruction(s) if opcode in _ONE_TO_ONE_INSTRUCTIONS: assembly.append(opcode.upper()) - elif opcode in ("alloca", "palloca"): + elif opcode in ("alloca", "palloca", "calloca"): pass elif opcode == "param": pass @@ -582,7 +579,7 @@ def _generate_evm_for_instruction( next_scheduled = next_liveness.last() cost = 0 - if not self.equivalence.equivalent(inst.output, next_scheduled): + if not self.dfg.are_equivalent(inst.output, next_scheduled): cost = self.swap_op(assembly, stack, next_scheduled) if DEBUG_SHOW_COST and cost != 0: From 88ddfb007e36372c825ca8fdb6d57d30838c5824 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 14:08:44 +0200 Subject: [PATCH 02/59] inliner cloning --- vyper/venom/basicblock.py | 33 +++++------------------ vyper/venom/function.py | 7 +++-- vyper/venom/passes/func_inliner.py | 43 ++++++++++++++++++++++++++++-- 3 files changed, 51 insertions(+), 32 deletions(-) diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index 5a22730a1e..31720da6ef 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -405,27 +405,12 @@ def get_ast_source(self) -> Optional[IRnode]: return inst.ast_source return self.parent.parent.ast_source - def copy(self, prefix: str = "") -> "IRInstruction": - ops: list[IROperand] = [] - for op in self.operands: - if isinstance(op, IRLabel): - ops.append(IRLabel(op.value)) - elif isinstance(op, IRVariable): - ops.append(IRVariable(f"{prefix}{op.name}")) - else: - ops.append(IRLiteral(op.value)) - - output = None - if self.output: - output = IRVariable(f"{prefix}{self.output.name}") - - inst = IRInstruction(self.opcode, ops, output) - inst.parent = self.parent - inst.liveness = self.liveness.copy() - inst.annotation = self.annotation - inst.ast_source = self.ast_source - inst.error_msg = self.error_msg - return inst + def copy(self) -> "IRInstruction": + ret = IRInstruction(self.opcode, self.operands.copy(), self.output) + ret.annotation = self.annotation + ret.ast_source = self.ast_source + ret.error_msg = self.error_msg + return ret def str_short(self) -> str: s = "" @@ -736,14 +721,10 @@ def liveness_in_vars(self) -> OrderedSet[IRVariable]: return OrderedSet() def copy(self, prefix: str = "") -> "IRBasicBlock": - new_label = IRLabel(f"{prefix}{self.label.value}") - bb = IRBasicBlock(new_label, self.parent) + bb = IRBasicBlock(self.label, self.parent) bb.instructions = [inst.copy(prefix) for inst in self.instructions] for inst in bb.instructions: inst.parent = bb - bb.cfg_in = OrderedSet() - bb.cfg_out = OrderedSet() - bb.out_vars = OrderedSet() return bb def __repr__(self) -> str: diff --git a/vyper/venom/function.py b/vyper/venom/function.py index 6069b9ec98..63301a8e14 100644 --- a/vyper/venom/function.py +++ b/vyper/venom/function.py @@ -212,11 +212,10 @@ def chain_basic_blocks(self) -> None: else: bb.append_instruction("stop") - def copy(self, prefix: str = ""): - new_label = IRLabel(f"{prefix}{self.name.value}") - new = IRFunction(new_label) + def copy(self): + new = IRFunction(self.name) for bb in self.get_basic_blocks(): - new_bb = bb.copy(prefix) + new_bb = bb.copy() new.append_basic_block(new_bb) return new diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index 639ee9ff01..92d590b3e6 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -6,7 +6,7 @@ from vyper.venom.analysis.cfg import CFGAnalysis from vyper.venom.analysis.dfg import DFGAnalysis from vyper.venom.analysis.fcg import FCGAnalysis -from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRLabel +from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRLabel, IRLiteral, IROperand, IRVariable from vyper.venom.function import IRFunction from vyper.venom.passes import FloatAllocas from vyper.venom.passes.base_pass import IRGlobalPass @@ -117,7 +117,7 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: call_site_return.insert_instruction(inst) call_site_func.append_basic_block(call_site_return) - func_copy = func.copy(prefix) + func_copy = self._clone_function(func, prefix) for bb in func_copy.get_basic_blocks(): bb.parent = call_site_func @@ -182,3 +182,42 @@ def dfs(fn): dfs(function) return OrderedSet(call_walk) + + def _clone_function(self, func: IRFunction, prefix: str) -> IRFunction: + new_func_label = IRLabel(f"{prefix}{func.name.value}") + clone = IRFunction(new_func_label) + for bb in func.get_basic_blocks(): + clone.append_basic_block(self._clone_basic_block(bb, prefix)) + return clone + + def _clone_basic_block(self, bb: IRBasicBlock, prefix: str) -> IRBasicBlock: + new_bb_label = IRLabel(f"{prefix}{bb.label.value}") + new_bb = IRBasicBlock(new_bb_label, bb.parent) + new_bb.instructions = [self._clone_instruction(inst, prefix) for inst in bb.instructions] + for inst in new_bb.instructions: + inst.parent = new_bb + return new_bb + + def _clone_instruction(self, inst: IRInstruction, prefix: str) -> IRInstruction: + ops: list[IROperand] = [] + for op in inst.operands: + if isinstance(op, IRLabel): + ops.append(IRLabel(op.value)) + elif isinstance(op, IRVariable): + ops.append(IRVariable(f"{prefix}{op.name}")) + else: + ops.append(IRLiteral(op.value)) + + output = None + if inst.output: + output = IRVariable(f"{prefix}{inst.output.name}") + + clone = IRInstruction(inst.opcode, ops, output) + clone.parent = inst.parent + clone.liveness = inst.liveness.copy() + clone.annotation = inst.annotation + clone.ast_source = inst.ast_source + clone.error_msg = inst.error_msg + + return clone + From 6435f8ba3a91b02eb90d83b6a61b6911edf44a69 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 14:27:13 +0200 Subject: [PATCH 03/59] bb copy fix --- vyper/venom/basicblock.py | 4 ++-- vyper/venom/passes/func_inliner.py | 12 +++++++++--- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index 31720da6ef..f04ae626a0 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -720,9 +720,9 @@ def liveness_in_vars(self) -> OrderedSet[IRVariable]: return inst.liveness return OrderedSet() - def copy(self, prefix: str = "") -> "IRBasicBlock": + def copy(self) -> "IRBasicBlock": bb = IRBasicBlock(self.label, self.parent) - bb.instructions = [inst.copy(prefix) for inst in self.instructions] + bb.instructions = [inst.copy() for inst in self.instructions] for inst in bb.instructions: inst.parent = bb return bb diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index 92d590b3e6..466ad881b6 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -6,7 +6,14 @@ from vyper.venom.analysis.cfg import CFGAnalysis from vyper.venom.analysis.dfg import DFGAnalysis from vyper.venom.analysis.fcg import FCGAnalysis -from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRLabel, IRLiteral, IROperand, IRVariable +from vyper.venom.basicblock import ( + IRBasicBlock, + IRInstruction, + IRLabel, + IRLiteral, + IROperand, + IRVariable, +) from vyper.venom.function import IRFunction from vyper.venom.passes import FloatAllocas from vyper.venom.passes.base_pass import IRGlobalPass @@ -189,7 +196,7 @@ def _clone_function(self, func: IRFunction, prefix: str) -> IRFunction: for bb in func.get_basic_blocks(): clone.append_basic_block(self._clone_basic_block(bb, prefix)) return clone - + def _clone_basic_block(self, bb: IRBasicBlock, prefix: str) -> IRBasicBlock: new_bb_label = IRLabel(f"{prefix}{bb.label.value}") new_bb = IRBasicBlock(new_bb_label, bb.parent) @@ -220,4 +227,3 @@ def _clone_instruction(self, inst: IRInstruction, prefix: str) -> IRInstruction: clone.error_msg = inst.error_msg return clone - From bc81fd6d4bbff377d3544ab2224491d4f10de1a5 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 14:33:16 +0200 Subject: [PATCH 04/59] ignore test --- tests/unit/compiler/test_source_map.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/compiler/test_source_map.py b/tests/unit/compiler/test_source_map.py index 974e4f2ece..a57fdcd317 100644 --- a/tests/unit/compiler/test_source_map.py +++ b/tests/unit/compiler/test_source_map.py @@ -129,7 +129,7 @@ def foo(i: uint256): assert "safemod" in error_map.values() -def test_error_map_not_overriding_errors(): +def test_error_map_not_overriding_errors(experimental_codegen): code = """ @external def foo(i: uint256): @@ -140,7 +140,8 @@ def bar(i: uint256) -> String[32]: return "foo foo" """ error_map = compile_code(code, output_formats=["source_map"])["source_map"]["error_map"] - assert "user revert with reason" in error_map.values() + if not experimental_codegen: + assert "user revert with reason" in error_map.values() assert "safemod" in error_map.values() From 580e667d63f6c917671596b8c81be970f0a2e2d5 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 14:36:51 +0200 Subject: [PATCH 05/59] mark xfail --- tests/unit/compiler/test_source_map.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/unit/compiler/test_source_map.py b/tests/unit/compiler/test_source_map.py index a57fdcd317..c82998a349 100644 --- a/tests/unit/compiler/test_source_map.py +++ b/tests/unit/compiler/test_source_map.py @@ -1,5 +1,6 @@ from collections import namedtuple +import pytest from vyper.compiler import compile_code from vyper.compiler.output import _compress_source_map from vyper.compiler.settings import OptimizationLevel @@ -129,7 +130,8 @@ def foo(i: uint256): assert "safemod" in error_map.values() -def test_error_map_not_overriding_errors(experimental_codegen): +@pytest.mark.venom_xfail(raises=AssertionError) +def test_error_map_not_overriding_errors(): code = """ @external def foo(i: uint256): @@ -140,8 +142,7 @@ def bar(i: uint256) -> String[32]: return "foo foo" """ error_map = compile_code(code, output_formats=["source_map"])["source_map"]["error_map"] - if not experimental_codegen: - assert "user revert with reason" in error_map.values() + assert "user revert with reason" in error_map.values() assert "safemod" in error_map.values() From 9dbbd6c8b829136502ff84d80be20ee1fd7b6db7 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 14:46:34 +0200 Subject: [PATCH 06/59] further cleanup --- tests/unit/compiler/test_source_map.py | 1 + vyper/venom/ir_node_to_venom.py | 26 +++----------------------- 2 files changed, 4 insertions(+), 23 deletions(-) diff --git a/tests/unit/compiler/test_source_map.py b/tests/unit/compiler/test_source_map.py index c82998a349..6f0a647fc1 100644 --- a/tests/unit/compiler/test_source_map.py +++ b/tests/unit/compiler/test_source_map.py @@ -1,6 +1,7 @@ from collections import namedtuple import pytest + from vyper.compiler import compile_code from vyper.compiler.output import _compress_source_map from vyper.compiler.settings import OptimizationLevel diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 4e772317ca..b8911829d9 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -195,21 +195,6 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio return return_buf -_current_func_t = None -_current_context = None - - -def _is_word_type(typ): - return typ._is_prim_word - # return typ.memory_bytes_required == 32 - - -# func_t: ContractFunctionT -def _returns_word(func_t) -> bool: - return_t = func_t.return_type - return return_t is not None and _is_word_type(return_t) - - def _handle_internal_func( # TODO: remove does_return_data, replace with `func_t.return_type is not None` fn: IRFunction, @@ -217,11 +202,9 @@ def _handle_internal_func( does_return_data: bool, symbols: SymbolTable, ) -> IRFunction: - global _alloca_table, _current_func_t, _current_context - - _current_func_t = ir.passthrough_metadata["func_t"] - _current_context = ir.passthrough_metadata["context"] + global _alloca_table + fn = fn.ctx.create_function(ir.args[0].args[0].value) fn = fn.ctx.create_function(ir.args[0].args[0].value) bb = fn.get_basic_block() @@ -594,10 +577,7 @@ def emit_body_blocks(): alloca = ir.passthrough_metadata["alloca"] assert alloca._callsite is not None if alloca._id not in _alloca_table: - bb = fn.get_basic_block() - ptr = IRLiteral(alloca.offset) - - _alloca_table[alloca._id] = ptr + _alloca_table[alloca._id] = IRLiteral(alloca.offset) ret = _alloca_table[alloca._id] # assumption: callocas appear in the same order as the # order of arguments to the function. From 42c0c2fed2ca2a2722000377c0065378479e6753 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 14:48:22 +0200 Subject: [PATCH 07/59] remove extra passes --- vyper/venom/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index 4b777c17fd..27cf85c119 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -87,8 +87,6 @@ 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() - RemoveUnusedVariablesPass(ac, fn).run_pass() StoreExpansionPass(ac, fn).run_pass() if optimize == OptimizationLevel.CODESIZE: From 60a792906f8a0697d4b1bb7d3c34346f615db225 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 14:52:38 +0200 Subject: [PATCH 08/59] cleanup callocas --- vyper/codegen/context.py | 3 --- vyper/codegen/self_call.py | 22 +--------------------- vyper/venom/README.md | 5 ----- vyper/venom/ir_node_to_venom.py | 18 +----------------- vyper/venom/passes/float_allocas.py | 2 +- vyper/venom/passes/sccp/sccp.py | 2 +- vyper/venom/venom_to_assembly.py | 4 ++-- 7 files changed, 6 insertions(+), 50 deletions(-) diff --git a/vyper/codegen/context.py b/vyper/codegen/context.py index 9562db7f03..7995b7b9f5 100644 --- a/vyper/codegen/context.py +++ b/vyper/codegen/context.py @@ -35,9 +35,6 @@ class Alloca: _id: int - # special metadata for calloca. hint for venom to tie calloca to call site. - _callsite: Optional[str] = None - def __post_init__(self): assert self.typ.memory_bytes_required == self.size diff --git a/vyper/codegen/self_call.py b/vyper/codegen/self_call.py index 32e5c84088..103fab2ceb 100644 --- a/vyper/codegen/self_call.py +++ b/vyper/codegen/self_call.py @@ -69,27 +69,7 @@ def ir_for_self_call(stmt_expr, context): # note: dst_tuple_t != args_tuple_t dst_tuple_t = TupleT(tuple(func_t.argument_types)) - if context.settings.experimental_codegen: - arg_items = ["multi"] - frame_info = func_t._ir_info.frame_info - - for var in frame_info.frame_vars.values(): - var = copy.copy(var) - alloca = var.alloca - assert alloca is not None - assert isinstance(var.pos, str) # help mypy - if not var.pos.startswith("$palloca"): - continue - newname = var.pos.replace("$palloca", "$calloca") - var.pos = newname - alloca = dataclasses.replace(alloca, _callsite=return_label) - irnode = var.as_ir_node() - irnode.passthrough_metadata["alloca"] = alloca - arg_items.append(irnode) - args_dst = IRnode.from_list(arg_items, typ=dst_tuple_t) - else: - # legacy - args_dst = IRnode(func_t._ir_info.frame_info.frame_start, typ=dst_tuple_t, location=MEMORY) + args_dst = IRnode(func_t._ir_info.frame_info.frame_start, typ=dst_tuple_t, location=MEMORY) # if one of the arguments is a self call, the argument # buffer could get borked. to prevent against that, diff --git a/vyper/venom/README.md b/vyper/venom/README.md index ff5e2888a4..964f52b524 100644 --- a/vyper/venom/README.md +++ b/vyper/venom/README.md @@ -205,11 +205,6 @@ Assembly can be inspected with `-f asm`, whereas an opcode view of the final byt out = palloca size, offset, id ``` - Like the `alloca` instruction but only used for parameters of internal functions which are passed by memory. -- `calloca` - - ``` - out = calloca size, offset, id, - ``` - - Similar to the `calloca` instruction but only used for parameters of internal functions which are passed by memory. Used at the call-site of a call. - `iload` - ``` out = iload offset diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index b8911829d9..01e6278304 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -111,8 +111,6 @@ SymbolTable = dict[str, IROperand] _alloca_table: dict[int, IROperand] -# assumption: callsites (return pc labels) are globally unique. -_callsites: dict[str, list[Alloca]] MAIN_ENTRY_LABEL_NAME = "__main_entry" @@ -120,9 +118,8 @@ def ir_node_to_venom(ir: IRnode) -> IRContext: _ = ir.unique_symbols # run unique symbols check - global _alloca_table, _callsites + global _alloca_table _alloca_table = {} - _callsites = defaultdict(list) ctx = IRContext() fn = ctx.create_function(MAIN_ENTRY_LABEL_NAME) @@ -165,7 +162,6 @@ def _append_return_args(fn: IRFunction, ofst: int = 0, size: int = 0): def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optional[IROperand]: - global _callsites setup_ir = ir.args[1] goto_ir = [ir for ir in ir.args if ir.value == "goto"][0] target_label = goto_ir.args[0].value # goto @@ -572,18 +568,6 @@ def emit_body_blocks(): _alloca_table[alloca._id] = ptr return _alloca_table[alloca._id] - elif ir.value.startswith("$calloca"): - global _callsites - alloca = ir.passthrough_metadata["alloca"] - assert alloca._callsite is not None - if alloca._id not in _alloca_table: - _alloca_table[alloca._id] = IRLiteral(alloca.offset) - ret = _alloca_table[alloca._id] - # assumption: callocas appear in the same order as the - # order of arguments to the function. - _callsites[alloca._callsite].append(alloca) - return ret - return symbols.get(ir.value) elif ir.is_literal: return IRLiteral(ir.value) diff --git a/vyper/venom/passes/float_allocas.py b/vyper/venom/passes/float_allocas.py index 794c1e497d..81fa115645 100644 --- a/vyper/venom/passes/float_allocas.py +++ b/vyper/venom/passes/float_allocas.py @@ -23,7 +23,7 @@ def run_pass(self): # Extract alloca instructions non_alloca_instructions = [] for inst in bb.instructions: - if inst.opcode in ("alloca", "palloca", "calloca"): + if inst.opcode in ("alloca", "palloca"): # note: order of allocas impacts bytecode. # TODO: investigate. entry_bb.insert_instruction(inst) diff --git a/vyper/venom/passes/sccp/sccp.py b/vyper/venom/passes/sccp/sccp.py index e4f9633d9f..83af176bee 100644 --- a/vyper/venom/passes/sccp/sccp.py +++ b/vyper/venom/passes/sccp/sccp.py @@ -178,7 +178,7 @@ def _visit_phi(self, inst: IRInstruction): def _visit_expr(self, inst: IRInstruction): opcode = inst.opcode - if opcode in ("store", "alloca", "palloca", "calloca"): + if opcode in ("store", "alloca", "palloca"): assert inst.output is not None, inst out = self._eval_from_lattice(inst.operands[0]) self._set_lattice(inst.output, out) diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index f49d97fd3f..dd0d2d1f63 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -367,7 +367,7 @@ def _generate_evm_for_instruction( if opcode in ["jmp", "djmp", "jnz", "invoke"]: operands = list(inst.get_non_label_operands()) - elif opcode in ("alloca", "palloca", "calloca"): + elif opcode in ("alloca", "palloca"): assert len(inst.operands) == 3, f"alloca/palloca must have 3 operands, got {inst}" offset, _size, _id = inst.operands operands = [offset] @@ -470,7 +470,7 @@ def _generate_evm_for_instruction( # Step 5: Emit the EVM instruction(s) if opcode in _ONE_TO_ONE_INSTRUCTIONS: assembly.append(opcode.upper()) - elif opcode in ("alloca", "palloca", "calloca"): + elif opcode in ("alloca", "palloca"): pass elif opcode == "param": pass From 1469b18d74f6bf4b39a41dff5fbe17ffdc941165 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 19 Feb 2025 13:58:37 +0100 Subject: [PATCH 09/59] fix source map for insert_instruction --- tests/unit/compiler/test_source_map.py | 1 - vyper/venom/basicblock.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/unit/compiler/test_source_map.py b/tests/unit/compiler/test_source_map.py index 6f0a647fc1..841afe4fa6 100644 --- a/tests/unit/compiler/test_source_map.py +++ b/tests/unit/compiler/test_source_map.py @@ -131,7 +131,6 @@ def foo(i: uint256): assert "safemod" in error_map.values() -@pytest.mark.venom_xfail(raises=AssertionError) def test_error_map_not_overriding_errors(): code = """ @external diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index f04ae626a0..f0e614c644 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -440,6 +440,10 @@ def __repr__(self) -> str: if self.annotation: s += f" ; {self.annotation}" + # debug: + # if self.error_msg: + # s += f" ;>>> {self.error_msg}" + return f"{s: <30}" @@ -568,8 +572,10 @@ def insert_instruction(self, instruction: IRInstruction, index: Optional[int] = assert not self.is_terminated, (self, instruction) index = len(self.instructions) instruction.parent = self - instruction.ast_source = self.parent.ast_source - instruction.error_msg = self.parent.error_msg + if instruction.ast_source is None: + instruction.ast_source = self.parent.ast_source + if instruction.error_msg is None: + instruction.error_msg = self.parent.error_msg self.instructions.insert(index, instruction) def mark_for_removal(self, instruction: IRInstruction) -> None: From e0afdc8751c420eaeef2e5c17e9c7af8faa4e622 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 14:59:40 +0200 Subject: [PATCH 10/59] cleanup memssa printer --- vyper/codegen/self_call.py | 3 --- vyper/venom/basicblock.py | 34 ++++++++------------------------- vyper/venom/ir_node_to_venom.py | 2 -- 3 files changed, 8 insertions(+), 31 deletions(-) diff --git a/vyper/codegen/self_call.py b/vyper/codegen/self_call.py index 103fab2ceb..a7182f6eba 100644 --- a/vyper/codegen/self_call.py +++ b/vyper/codegen/self_call.py @@ -1,6 +1,3 @@ -import copy -import dataclasses - from vyper.codegen.core import _freshname, eval_once_check, make_setter from vyper.codegen.ir_node import IRnode from vyper.evm.address_space import MEMORY diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index f04ae626a0..7fadd7832f 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -1,6 +1,5 @@ import json import re -from contextvars import ContextVar from typing import TYPE_CHECKING, Any, Iterator, Optional, Union import vyper.venom.effects as effects @@ -99,8 +98,6 @@ if TYPE_CHECKING: from vyper.venom.function import IRFunction -ir_printer = ContextVar("ir_printer", default=None) - def flip_comparison_opcode(opcode): if opcode in ("gt", "sgt"): @@ -728,27 +725,12 @@ def copy(self) -> "IRBasicBlock": return bb def __repr__(self) -> str: - printer = ir_printer.get() - - s = ( - f"{repr(self.label)}: ; IN={[bb.label for bb in self.cfg_in]}" - f" OUT={[bb.label for bb in self.cfg_out]} => {self.out_vars}\n" - ) - if printer and hasattr(printer, "_pre_block"): - s += printer._pre_block(self) - for inst in self.instructions: - if printer and hasattr(printer, "_pre_instruction"): - s += printer._pre_instruction(inst) - s += f" {str(inst).strip()}" - if printer and hasattr(printer, "_post_instruction"): - s += printer._post_instruction(inst) - s += "\n" + s = f"{self.label}: ; IN={[bb.label for bb in self.cfg_in]}" + s += f" OUT={[bb.label for bb in self.cfg_out]} => {self.out_vars}\n" + for instruction in self.instructions: + s += f" {str(instruction).strip()}\n" + if len(self.instructions) > 30: + s += f" ; {self.label}\n" + if len(self.instructions) > 30 or self.parent.num_basic_blocks > 5: + s += f" ; ({self.parent.name})\n\n" return s - - -class IRPrinter: - def _pre_instruction(self, inst: IRInstruction) -> str: - return "" - - def _post_instruction(self, inst: IRInstruction) -> str: - return "" diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 01e6278304..cb4b1d623e 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -1,9 +1,7 @@ import functools import re -from collections import defaultdict from typing import Optional -from vyper.codegen.context import Alloca from vyper.codegen.ir_node import IRnode from vyper.evm.opcodes import get_opcodes from vyper.venom.basicblock import ( From 6de703b591a9f0823f198684e0b6bb9e4b4948d5 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 15:10:34 +0200 Subject: [PATCH 11/59] lint --- tests/unit/compiler/test_source_map.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/unit/compiler/test_source_map.py b/tests/unit/compiler/test_source_map.py index 841afe4fa6..974e4f2ece 100644 --- a/tests/unit/compiler/test_source_map.py +++ b/tests/unit/compiler/test_source_map.py @@ -1,7 +1,5 @@ from collections import namedtuple -import pytest - from vyper.compiler import compile_code from vyper.compiler.output import _compress_source_map from vyper.compiler.settings import OptimizationLevel From 0fee1a65f9a3d2cce254e4d5d1f5bb2430ac6a1b Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Wed, 19 Feb 2025 15:40:16 +0100 Subject: [PATCH 12/59] remove dead function --- vyper/venom/basicblock.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index 53413c6db0..2039388c70 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -593,8 +593,6 @@ def ensure_well_formed(self): self.remove_instructions_after(inst) self.append_instruction("stop") # TODO: make revert a bb terminator? break - # TODO: remove once clear_dead_instructions is removed - self.clear_dead_instructions() def key(inst): if inst.opcode in ("phi", "param"): From a3b26aa3adf4ee900757c01e9c226769d51bc430 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 16:57:57 +0200 Subject: [PATCH 13/59] new call conv --- vyper/venom/ir_node_to_venom.py | 129 +++++++++++++++++++++++++++-- vyper/venom/passes/func_inliner.py | 8 ++ vyper/venom/venom_to_assembly.py | 37 +++++++-- 3 files changed, 162 insertions(+), 12 deletions(-) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index cb4b1d623e..8f5c0769fd 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -13,7 +13,9 @@ IRVariable, ) from vyper.venom.context import IRContext -from vyper.venom.function import IRFunction +from vyper.venom.function import IRFunction, IRParameter + +ENABLE_NEW_CALL_CONV = True # Instructions that are mapped to their inverse INVERSE_MAPPED_IR_INSTRUCTIONS = {"ne": "eq", "le": "gt", "sle": "sgt", "ge": "lt", "sge": "slt"} @@ -167,6 +169,10 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio func_t = ir.passthrough_metadata["func_t"] assert func_t is not None, "func_t not found in passthrough metadata" + returns_word = _returns_word(func_t) + + stack_args: list[IROperand] = [] + if setup_ir != goto_ir: _convert_ir_bb(fn, setup_ir, symbols) @@ -174,6 +180,7 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio callsite_op = converted_args[-1] assert isinstance(callsite_op, IRLabel), converted_args + callsite = callsite_op.value bb = fn.get_basic_block() return_buf = None @@ -182,7 +189,27 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio return_buf = converted_args[0] if return_buf is not None: - ret_args.append(return_buf) # type: ignore + if not ENABLE_NEW_CALL_CONV or not returns_word: + ret_args.append(return_buf) # type: ignore + + callsite_args = _callsites[callsite] + stack_args = [] + if ENABLE_NEW_CALL_CONV: + for alloca in callsite_args: + if not _is_word_type(alloca.typ): + continue + ptr = _alloca_table[alloca._id] + stack_arg = bb.append_instruction("mload", ptr) + assert stack_arg is not None + stack_args.append(stack_arg) + ret_args.extend(stack_args) + + if returns_word: + ret_value = bb.append_invoke_instruction(ret_args, returns=True) # type: ignore + assert ret_value is not None + assert isinstance(return_buf, IROperand) + bb.append_instruction("mstore", ret_value, return_buf) + return return_buf bb.append_invoke_instruction(ret_args, returns=False) # type: ignore @@ -196,24 +223,59 @@ def _handle_internal_func( does_return_data: bool, symbols: SymbolTable, ) -> IRFunction: - global _alloca_table + global _alloca_table, _current_func_t, _current_context + + _current_func_t = ir.passthrough_metadata["func_t"] + _current_context = ir.passthrough_metadata["context"] + func_t = _current_func_t + context = _current_context fn = fn.ctx.create_function(ir.args[0].args[0].value) fn = fn.ctx.create_function(ir.args[0].args[0].value) + if ENABLE_NEW_CALL_CONV: + index = 0 + if func_t.return_type is not None and not _returns_word(func_t): + index += 1 + for arg in func_t.arguments: + var = context.lookup_var(arg.name) + if not _is_word_type(var.typ): + continue + venom_arg = IRParameter( + var.name, index, var.alloca.offset, var.alloca.size, None, None, None + ) + fn.args.append(venom_arg) + index += 1 + bb = fn.get_basic_block() _saved_alloca_table = _alloca_table _alloca_table = {} + returns_word = _returns_word(func_t) + # return buffer if does_return_data: - buf = bb.append_instruction("param") - bb.instructions[-1].annotation = "return_buffer" + if ENABLE_NEW_CALL_CONV and returns_word: + # this alloca should be stripped by mem2var. we can remove + # the hardcoded offset once we have proper memory allocator + # functionality in venom. + buf = bb.append_instruction("alloca", IRLiteral(-1), IRLiteral(-1), IRLiteral(-1)) + else: + buf = bb.append_instruction("param") + bb.instructions[-1].annotation = "return_buffer" assert buf is not None # help mypy symbols["return_buffer"] = buf + if ENABLE_NEW_CALL_CONV: + for arg in fn.args: + ret = bb.append_instruction("param") + bb.instructions[-1].annotation = arg.name + assert ret is not None # help mypy + symbols[arg.name] = ret + arg.func_var = ret + # return address return_pc = bb.append_instruction("param") assert return_pc is not None # help mypy @@ -221,9 +283,21 @@ def _handle_internal_func( bb.instructions[-1].annotation = "return_pc" + if ENABLE_NEW_CALL_CONV: + for arg in fn.args: + var = IRVariable(arg.name) + bb.append_instruction("store", IRLiteral(arg.offset), ret=var) # type: ignore + arg.addr_var = var + _convert_ir_bb(fn, ir.args[0].args[2], symbols) _alloca_table = _saved_alloca_table + # if ENABLE_NEW_CALL_CONV: + # for inst in bb.instructions: + # if inst.opcode == "store": + # param = fn.get_param_at_offset(inst.operands[0].value) + # if param is not None: + # inst.operands[0] = param.addr_var # type: ignore return fn @@ -435,7 +509,12 @@ def _convert_ir_bb(fn, ir, symbols): if label.value == "return_pc": label = symbols.get("return_pc") # return label should be top of stack - bb.append_instruction("ret", label) + if _returns_word(_current_func_t) and ENABLE_NEW_CALL_CONV: + buf = symbols["return_buffer"] + val = bb.append_instruction("mload", buf) + bb.append_instruction("ret", val, label) + else: + bb.append_instruction("ret", label) else: bb.append_instruction("jmp", label) @@ -444,11 +523,29 @@ def _convert_ir_bb(fn, ir, symbols): # to fix upstream. val, ptr = _convert_ir_bb_list(fn, reversed(ir.args), symbols) + if ENABLE_NEW_CALL_CONV: + if isinstance(ptr, IRVariable): + # TODO: is this bad code? + param = fn.get_param_by_name(ptr) + if param is not None: + return fn.get_basic_block().append_instruction("store", val, ret=param.func_var) + + if isinstance(ptr, IRLabel) and ptr.value.startswith("$palloca"): + symbol = symbols.get(ptr.annotation, None) + if symbol is not None: + return fn.get_basic_block().append_instruction("store", symbol) + return fn.get_basic_block().append_instruction("mstore", val, ptr) elif ir.value == "mload": arg = ir.args[0] ptr = _convert_ir_bb(fn, arg, symbols) + if ENABLE_NEW_CALL_CONV: + if isinstance(arg.value, str) and arg.value.startswith("$palloca"): + symbol = symbols.get(arg.annotation, None) + if symbol is not None: + return fn.get_basic_block().append_instruction("store", symbol) + return fn.get_basic_block().append_instruction("mload", ptr) elif ir.value == "ceil32": x = ir.args[0] @@ -559,6 +656,8 @@ def emit_body_blocks(): elif ir.value.startswith("$palloca"): alloca = ir.passthrough_metadata["alloca"] + if ENABLE_NEW_CALL_CONV and fn.get_param_at_offset(alloca.offset) is not None: + return fn.get_param_at_offset(alloca.offset).addr_var if alloca._id not in _alloca_table: ptr = fn.get_basic_block().append_instruction( "palloca", alloca.offset, alloca.size, alloca._id @@ -566,6 +665,24 @@ def emit_body_blocks(): _alloca_table[alloca._id] = ptr return _alloca_table[alloca._id] + elif ir.value.startswith("$calloca"): + global _callsites + alloca = ir.passthrough_metadata["alloca"] + assert alloca._callsite is not None + if alloca._id not in _alloca_table: + bb = fn.get_basic_block() + if ENABLE_NEW_CALL_CONV and _is_word_type(alloca.typ): + ptr = bb.append_instruction("alloca", alloca.offset, alloca.size, alloca._id) + else: + ptr = IRLiteral(alloca.offset) + + _alloca_table[alloca._id] = ptr + ret = _alloca_table[alloca._id] + # assumption: callocas appear in the same order as the + # order of arguments to the function. + _callsites[alloca._callsite].append(alloca) + return ret + return symbols.get(ir.value) elif ir.is_literal: return IRLiteral(ir.value) diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index 466ad881b6..1022558682 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -1,5 +1,6 @@ from typing import List, Optional +from vyper.venom.ir_node_to_venom import ENABLE_NEW_CALL_CONV from vyper.compiler.settings import OptimizationLevel from vyper.exceptions import CompilerPanic from vyper.utils import OrderedSet @@ -154,6 +155,13 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: ): inst.make_nop() elif inst.opcode == "ret": + if len(inst.operands) > 1: + # sanity check (should remove once new callconv stabilizes) + assert ENABLE_NEW_CALL_CONV + ret_value = inst.operands[0] + bb.insert_instruction( + IRInstruction("store", [ret_value], call_site.output), -1 + ) inst.opcode = "jmp" inst.operands = [call_site_return.label] elif inst.opcode in ["jmp", "jnz", "djmp", "phi"]: diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index dd0d2d1f63..7b0edc7271 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -21,6 +21,7 @@ IRVariable, ) from vyper.venom.context import IRContext +from vyper.venom.ir_node_to_venom import ENABLE_NEW_CALL_CONV from vyper.venom.passes import NormalizationPass from vyper.venom.stack_model import StackModel @@ -302,14 +303,38 @@ def _generate_evm_for_basicblock_r( if len(basicblock.cfg_in) == 1: self.clean_stack_from_cfg_in(asm, basicblock, stack) - all_insts = sorted(basicblock.instructions, key=lambda x: x.opcode != "param") + if ENABLE_NEW_CALL_CONV: + param_insts = [inst for inst in basicblock.instructions if inst.opcode == "param"] + body_insts = [inst for inst in basicblock.instructions if inst.opcode != "param"] + + params_to_pop = [] + for inst in param_insts: + assert isinstance(inst.output, IRVariable) + stack.push(inst.output) + if len(self.dfg.get_uses(inst.output)) == 0: + params_to_pop.append(inst.output) + + for param in params_to_pop: + depth = stack.get_depth(param) + if depth != StackModel.NOT_IN_STACK: + self.swap(asm, stack, depth) + self.pop(asm, stack) + + for i, inst in enumerate(body_insts): + next_liveness = ( + body_insts[i + 1].liveness if i + 1 < len(body_insts) else basicblock.out_vars + ) - for i, inst in enumerate(all_insts): - next_liveness = ( - all_insts[i + 1].liveness if i + 1 < len(all_insts) else basicblock.out_vars - ) + asm.extend(self._generate_evm_for_instruction(inst, stack, next_liveness)) + else: + all_insts = sorted(basicblock.instructions, key=lambda x: x.opcode != "param") + + for i, inst in enumerate(all_insts): + next_liveness = ( + all_insts[i + 1].liveness if i + 1 < len(all_insts) else basicblock.out_vars + ) - asm.extend(self._generate_evm_for_instruction(inst, stack, next_liveness)) + asm.extend(self._generate_evm_for_instruction(inst, stack, next_liveness)) if DEBUG_SHOW_COST: print(" ".join(map(str, asm)), file=sys.stderr) From 1bb40fed9011c70d7149a81f31de146ace51acda Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 17:01:10 +0200 Subject: [PATCH 14/59] Revert "further cleanup" This reverts commit 9dbbd6c8b829136502ff84d80be20ee1fd7b6db7. --- vyper/codegen/context.py | 3 +++ vyper/codegen/self_call.py | 25 ++++++++++++++++++++++++- vyper/venom/README.md | 5 +++++ vyper/venom/ir_node_to_venom.py | 29 +++++++++++++++++++++++------ vyper/venom/passes/float_allocas.py | 2 +- vyper/venom/passes/sccp/sccp.py | 2 +- vyper/venom/venom_to_assembly.py | 4 ++-- 7 files changed, 59 insertions(+), 11 deletions(-) diff --git a/vyper/codegen/context.py b/vyper/codegen/context.py index 7995b7b9f5..9562db7f03 100644 --- a/vyper/codegen/context.py +++ b/vyper/codegen/context.py @@ -35,6 +35,9 @@ class Alloca: _id: int + # special metadata for calloca. hint for venom to tie calloca to call site. + _callsite: Optional[str] = None + def __post_init__(self): assert self.typ.memory_bytes_required == self.size diff --git a/vyper/codegen/self_call.py b/vyper/codegen/self_call.py index a7182f6eba..32e5c84088 100644 --- a/vyper/codegen/self_call.py +++ b/vyper/codegen/self_call.py @@ -1,3 +1,6 @@ +import copy +import dataclasses + from vyper.codegen.core import _freshname, eval_once_check, make_setter from vyper.codegen.ir_node import IRnode from vyper.evm.address_space import MEMORY @@ -66,7 +69,27 @@ def ir_for_self_call(stmt_expr, context): # note: dst_tuple_t != args_tuple_t dst_tuple_t = TupleT(tuple(func_t.argument_types)) - args_dst = IRnode(func_t._ir_info.frame_info.frame_start, typ=dst_tuple_t, location=MEMORY) + if context.settings.experimental_codegen: + arg_items = ["multi"] + frame_info = func_t._ir_info.frame_info + + for var in frame_info.frame_vars.values(): + var = copy.copy(var) + alloca = var.alloca + assert alloca is not None + assert isinstance(var.pos, str) # help mypy + if not var.pos.startswith("$palloca"): + continue + newname = var.pos.replace("$palloca", "$calloca") + var.pos = newname + alloca = dataclasses.replace(alloca, _callsite=return_label) + irnode = var.as_ir_node() + irnode.passthrough_metadata["alloca"] = alloca + arg_items.append(irnode) + args_dst = IRnode.from_list(arg_items, typ=dst_tuple_t) + else: + # legacy + args_dst = IRnode(func_t._ir_info.frame_info.frame_start, typ=dst_tuple_t, location=MEMORY) # if one of the arguments is a self call, the argument # buffer could get borked. to prevent against that, diff --git a/vyper/venom/README.md b/vyper/venom/README.md index 964f52b524..ff5e2888a4 100644 --- a/vyper/venom/README.md +++ b/vyper/venom/README.md @@ -205,6 +205,11 @@ Assembly can be inspected with `-f asm`, whereas an opcode view of the final byt out = palloca size, offset, id ``` - Like the `alloca` instruction but only used for parameters of internal functions which are passed by memory. +- `calloca` + - ``` + out = calloca size, offset, id, + ``` + - Similar to the `calloca` instruction but only used for parameters of internal functions which are passed by memory. Used at the call-site of a call. - `iload` - ``` out = iload offset diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 8f5c0769fd..03251341f1 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -1,7 +1,9 @@ +from collections import defaultdict import functools import re from typing import Optional +from vyper.codegen.context import Alloca from vyper.codegen.ir_node import IRnode from vyper.evm.opcodes import get_opcodes from vyper.venom.basicblock import ( @@ -111,6 +113,8 @@ SymbolTable = dict[str, IROperand] _alloca_table: dict[int, IROperand] +# assumption: callsites (return pc labels) are globally unique. +_callsites: dict[str, list[Alloca]] MAIN_ENTRY_LABEL_NAME = "__main_entry" @@ -118,8 +122,9 @@ def ir_node_to_venom(ir: IRnode) -> IRContext: _ = ir.unique_symbols # run unique symbols check - global _alloca_table + global _alloca_table, _callsites _alloca_table = {} + _callsites = defaultdict(list) ctx = IRContext() fn = ctx.create_function(MAIN_ENTRY_LABEL_NAME) @@ -162,6 +167,7 @@ def _append_return_args(fn: IRFunction, ofst: int = 0, size: int = 0): def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optional[IROperand]: + global _callsites setup_ir = ir.args[1] goto_ir = [ir for ir in ir.args if ir.value == "goto"][0] target_label = goto_ir.args[0].value # goto @@ -216,6 +222,21 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio return return_buf +_current_func_t = None +_current_context = None + + +def _is_word_type(typ): + return typ._is_prim_word + # return typ.memory_bytes_required == 32 + + +# func_t: ContractFunctionT +def _returns_word(func_t) -> bool: + return_t = func_t.return_type + return return_t is not None and _is_word_type(return_t) + + def _handle_internal_func( # TODO: remove does_return_data, replace with `func_t.return_type is not None` fn: IRFunction, @@ -230,7 +251,6 @@ def _handle_internal_func( func_t = _current_func_t context = _current_context - fn = fn.ctx.create_function(ir.args[0].args[0].value) fn = fn.ctx.create_function(ir.args[0].args[0].value) if ENABLE_NEW_CALL_CONV: @@ -671,10 +691,7 @@ def emit_body_blocks(): assert alloca._callsite is not None if alloca._id not in _alloca_table: bb = fn.get_basic_block() - if ENABLE_NEW_CALL_CONV and _is_word_type(alloca.typ): - ptr = bb.append_instruction("alloca", alloca.offset, alloca.size, alloca._id) - else: - ptr = IRLiteral(alloca.offset) + ptr = IRLiteral(alloca.offset) _alloca_table[alloca._id] = ptr ret = _alloca_table[alloca._id] diff --git a/vyper/venom/passes/float_allocas.py b/vyper/venom/passes/float_allocas.py index 81fa115645..794c1e497d 100644 --- a/vyper/venom/passes/float_allocas.py +++ b/vyper/venom/passes/float_allocas.py @@ -23,7 +23,7 @@ def run_pass(self): # Extract alloca instructions non_alloca_instructions = [] for inst in bb.instructions: - if inst.opcode in ("alloca", "palloca"): + if inst.opcode in ("alloca", "palloca", "calloca"): # note: order of allocas impacts bytecode. # TODO: investigate. entry_bb.insert_instruction(inst) diff --git a/vyper/venom/passes/sccp/sccp.py b/vyper/venom/passes/sccp/sccp.py index 83af176bee..e4f9633d9f 100644 --- a/vyper/venom/passes/sccp/sccp.py +++ b/vyper/venom/passes/sccp/sccp.py @@ -178,7 +178,7 @@ def _visit_phi(self, inst: IRInstruction): def _visit_expr(self, inst: IRInstruction): opcode = inst.opcode - if opcode in ("store", "alloca", "palloca"): + if opcode in ("store", "alloca", "palloca", "calloca"): assert inst.output is not None, inst out = self._eval_from_lattice(inst.operands[0]) self._set_lattice(inst.output, out) diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index 7b0edc7271..e859a73663 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -392,7 +392,7 @@ def _generate_evm_for_instruction( if opcode in ["jmp", "djmp", "jnz", "invoke"]: operands = list(inst.get_non_label_operands()) - elif opcode in ("alloca", "palloca"): + elif opcode in ("alloca", "palloca", "calloca"): assert len(inst.operands) == 3, f"alloca/palloca must have 3 operands, got {inst}" offset, _size, _id = inst.operands operands = [offset] @@ -495,7 +495,7 @@ def _generate_evm_for_instruction( # Step 5: Emit the EVM instruction(s) if opcode in _ONE_TO_ONE_INSTRUCTIONS: assembly.append(opcode.upper()) - elif opcode in ("alloca", "palloca"): + elif opcode in ("alloca", "palloca", "calloca"): pass elif opcode == "param": pass From d2e93451e3a1a7be28b1ea2d4462a312ede088b8 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 17:20:35 +0200 Subject: [PATCH 15/59] call conv --- vyper/venom/ir_node_to_venom.py | 123 +++++++++++++---------------- vyper/venom/parser.py | 2 +- vyper/venom/passes/func_inliner.py | 3 - vyper/venom/venom_to_assembly.py | 49 +++++------- 4 files changed, 75 insertions(+), 102 deletions(-) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 03251341f1..c0c3619cca 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -1,6 +1,6 @@ -from collections import defaultdict import functools import re +from collections import defaultdict from typing import Optional from vyper.codegen.context import Alloca @@ -17,8 +17,6 @@ from vyper.venom.context import IRContext from vyper.venom.function import IRFunction, IRParameter -ENABLE_NEW_CALL_CONV = True - # Instructions that are mapped to their inverse INVERSE_MAPPED_IR_INSTRUCTIONS = {"ne": "eq", "le": "gt", "sle": "sgt", "ge": "lt", "sge": "slt"} @@ -195,27 +193,27 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio return_buf = converted_args[0] if return_buf is not None: - if not ENABLE_NEW_CALL_CONV or not returns_word: + if not returns_word: ret_args.append(return_buf) # type: ignore callsite_args = _callsites[callsite] stack_args = [] - if ENABLE_NEW_CALL_CONV: - for alloca in callsite_args: - if not _is_word_type(alloca.typ): - continue - ptr = _alloca_table[alloca._id] - stack_arg = bb.append_instruction("mload", ptr) - assert stack_arg is not None - stack_args.append(stack_arg) - ret_args.extend(stack_args) - if returns_word: - ret_value = bb.append_invoke_instruction(ret_args, returns=True) # type: ignore - assert ret_value is not None - assert isinstance(return_buf, IROperand) - bb.append_instruction("mstore", ret_value, return_buf) - return return_buf + for alloca in callsite_args: + if not _is_word_type(alloca.typ): + continue + ptr = _alloca_table[alloca._id] + stack_arg = bb.append_instruction("mload", ptr) + assert stack_arg is not None + stack_args.append(stack_arg) + ret_args.extend(stack_args) + + if returns_word: + ret_value = bb.append_invoke_instruction(ret_args, returns=True) # type: ignore + assert ret_value is not None + assert isinstance(return_buf, IROperand) + bb.append_instruction("mstore", ret_value, return_buf) + return return_buf bb.append_invoke_instruction(ret_args, returns=False) # type: ignore @@ -253,19 +251,18 @@ def _handle_internal_func( fn = fn.ctx.create_function(ir.args[0].args[0].value) - if ENABLE_NEW_CALL_CONV: - index = 0 - if func_t.return_type is not None and not _returns_word(func_t): - index += 1 - for arg in func_t.arguments: - var = context.lookup_var(arg.name) - if not _is_word_type(var.typ): - continue - venom_arg = IRParameter( - var.name, index, var.alloca.offset, var.alloca.size, None, None, None - ) - fn.args.append(venom_arg) - index += 1 + index = 0 + if func_t.return_type is not None and not _returns_word(func_t): + index += 1 + for arg in func_t.arguments: + var = context.lookup_var(arg.name) + if not _is_word_type(var.typ): + continue + venom_arg = IRParameter( + var.name, index, var.alloca.offset, var.alloca.size, None, None, None + ) + fn.args.append(venom_arg) + index += 1 bb = fn.get_basic_block() @@ -276,7 +273,7 @@ def _handle_internal_func( # return buffer if does_return_data: - if ENABLE_NEW_CALL_CONV and returns_word: + if returns_word: # this alloca should be stripped by mem2var. we can remove # the hardcoded offset once we have proper memory allocator # functionality in venom. @@ -288,13 +285,12 @@ def _handle_internal_func( assert buf is not None # help mypy symbols["return_buffer"] = buf - if ENABLE_NEW_CALL_CONV: - for arg in fn.args: - ret = bb.append_instruction("param") - bb.instructions[-1].annotation = arg.name - assert ret is not None # help mypy - symbols[arg.name] = ret - arg.func_var = ret + for arg in fn.args: + ret = bb.append_instruction("param") + bb.instructions[-1].annotation = arg.name + assert ret is not None # help mypy + symbols[arg.name] = ret + arg.func_var = ret # return address return_pc = bb.append_instruction("param") @@ -303,21 +299,14 @@ def _handle_internal_func( bb.instructions[-1].annotation = "return_pc" - if ENABLE_NEW_CALL_CONV: - for arg in fn.args: - var = IRVariable(arg.name) - bb.append_instruction("store", IRLiteral(arg.offset), ret=var) # type: ignore - arg.addr_var = var + for arg in fn.args: + var = IRVariable(arg.name) + bb.append_instruction("store", IRLiteral(arg.offset), ret=var) # type: ignore + arg.addr_var = var _convert_ir_bb(fn, ir.args[0].args[2], symbols) _alloca_table = _saved_alloca_table - # if ENABLE_NEW_CALL_CONV: - # for inst in bb.instructions: - # if inst.opcode == "store": - # param = fn.get_param_at_offset(inst.operands[0].value) - # if param is not None: - # inst.operands[0] = param.addr_var # type: ignore return fn @@ -529,7 +518,7 @@ def _convert_ir_bb(fn, ir, symbols): if label.value == "return_pc": label = symbols.get("return_pc") # return label should be top of stack - if _returns_word(_current_func_t) and ENABLE_NEW_CALL_CONV: + if _returns_word(_current_func_t): buf = symbols["return_buffer"] val = bb.append_instruction("mload", buf) bb.append_instruction("ret", val, label) @@ -543,28 +532,26 @@ def _convert_ir_bb(fn, ir, symbols): # to fix upstream. val, ptr = _convert_ir_bb_list(fn, reversed(ir.args), symbols) - if ENABLE_NEW_CALL_CONV: - if isinstance(ptr, IRVariable): - # TODO: is this bad code? - param = fn.get_param_by_name(ptr) - if param is not None: - return fn.get_basic_block().append_instruction("store", val, ret=param.func_var) + if isinstance(ptr, IRVariable): + # TODO: is this bad code? + param = fn.get_param_by_name(ptr) + if param is not None: + return fn.get_basic_block().append_instruction("store", val, ret=param.func_var) - if isinstance(ptr, IRLabel) and ptr.value.startswith("$palloca"): - symbol = symbols.get(ptr.annotation, None) - if symbol is not None: - return fn.get_basic_block().append_instruction("store", symbol) + if isinstance(ptr, IRLabel) and ptr.value.startswith("$palloca"): + symbol = symbols.get(ptr.annotation, None) + if symbol is not None: + return fn.get_basic_block().append_instruction("store", symbol) return fn.get_basic_block().append_instruction("mstore", val, ptr) elif ir.value == "mload": arg = ir.args[0] ptr = _convert_ir_bb(fn, arg, symbols) - if ENABLE_NEW_CALL_CONV: - if isinstance(arg.value, str) and arg.value.startswith("$palloca"): - symbol = symbols.get(arg.annotation, None) - if symbol is not None: - return fn.get_basic_block().append_instruction("store", symbol) + if isinstance(arg.value, str) and arg.value.startswith("$palloca"): + symbol = symbols.get(arg.annotation, None) + if symbol is not None: + return fn.get_basic_block().append_instruction("store", symbol) return fn.get_basic_block().append_instruction("mload", ptr) elif ir.value == "ceil32": @@ -676,7 +663,7 @@ def emit_body_blocks(): elif ir.value.startswith("$palloca"): alloca = ir.passthrough_metadata["alloca"] - if ENABLE_NEW_CALL_CONV and fn.get_param_at_offset(alloca.offset) is not None: + if fn.get_param_at_offset(alloca.offset) is not None: return fn.get_param_at_offset(alloca.offset).addr_var if alloca._id not in _alloca_table: ptr = fn.get_basic_block().append_instruction( diff --git a/vyper/venom/parser.py b/vyper/venom/parser.py index 2b47b2e335..6ff63c08a6 100644 --- a/vyper/venom/parser.py +++ b/vyper/venom/parser.py @@ -49,7 +49,7 @@ CONST: SIGNED_INT OPCODE: CNAME - VAR_IDENT: "%" (DIGIT|LETTER|"_"|":"|"%"|"@")+ + VAR_IDENT: "%" (DIGIT|LETTER|"_"|"-"|":"|"%"|"@")+ # handy for identifier to be an escaped string sometimes # (especially for machine-generated labels) diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index 1022558682..b1ef2e5560 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -1,6 +1,5 @@ from typing import List, Optional -from vyper.venom.ir_node_to_venom import ENABLE_NEW_CALL_CONV from vyper.compiler.settings import OptimizationLevel from vyper.exceptions import CompilerPanic from vyper.utils import OrderedSet @@ -156,8 +155,6 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: inst.make_nop() elif inst.opcode == "ret": if len(inst.operands) > 1: - # sanity check (should remove once new callconv stabilizes) - assert ENABLE_NEW_CALL_CONV ret_value = inst.operands[0] bb.insert_instruction( IRInstruction("store", [ret_value], call_site.output), -1 diff --git a/vyper/venom/venom_to_assembly.py b/vyper/venom/venom_to_assembly.py index e859a73663..30990dfb0f 100644 --- a/vyper/venom/venom_to_assembly.py +++ b/vyper/venom/venom_to_assembly.py @@ -21,7 +21,6 @@ IRVariable, ) from vyper.venom.context import IRContext -from vyper.venom.ir_node_to_venom import ENABLE_NEW_CALL_CONV from vyper.venom.passes import NormalizationPass from vyper.venom.stack_model import StackModel @@ -303,38 +302,28 @@ def _generate_evm_for_basicblock_r( if len(basicblock.cfg_in) == 1: self.clean_stack_from_cfg_in(asm, basicblock, stack) - if ENABLE_NEW_CALL_CONV: - param_insts = [inst for inst in basicblock.instructions if inst.opcode == "param"] - body_insts = [inst for inst in basicblock.instructions if inst.opcode != "param"] - - params_to_pop = [] - for inst in param_insts: - assert isinstance(inst.output, IRVariable) - stack.push(inst.output) - if len(self.dfg.get_uses(inst.output)) == 0: - params_to_pop.append(inst.output) - - for param in params_to_pop: - depth = stack.get_depth(param) - if depth != StackModel.NOT_IN_STACK: - self.swap(asm, stack, depth) - self.pop(asm, stack) - - for i, inst in enumerate(body_insts): - next_liveness = ( - body_insts[i + 1].liveness if i + 1 < len(body_insts) else basicblock.out_vars - ) + param_insts = [inst for inst in basicblock.instructions if inst.opcode == "param"] + body_insts = [inst for inst in basicblock.instructions if inst.opcode != "param"] - asm.extend(self._generate_evm_for_instruction(inst, stack, next_liveness)) - else: - all_insts = sorted(basicblock.instructions, key=lambda x: x.opcode != "param") + params_to_pop = [] + for inst in param_insts: + assert isinstance(inst.output, IRVariable) + stack.push(inst.output) + if len(self.dfg.get_uses(inst.output)) == 0: + params_to_pop.append(inst.output) - for i, inst in enumerate(all_insts): - next_liveness = ( - all_insts[i + 1].liveness if i + 1 < len(all_insts) else basicblock.out_vars - ) + for param in params_to_pop: + depth = stack.get_depth(param) + if depth != StackModel.NOT_IN_STACK: + self.swap(asm, stack, depth) + self.pop(asm, stack) + + for i, inst in enumerate(body_insts): + next_liveness = ( + body_insts[i + 1].liveness if i + 1 < len(body_insts) else basicblock.out_vars + ) - asm.extend(self._generate_evm_for_instruction(inst, stack, next_liveness)) + asm.extend(self._generate_evm_for_instruction(inst, stack, next_liveness)) if DEBUG_SHOW_COST: print(" ".join(map(str, asm)), file=sys.stderr) From 6f655535dc5fd3c3a1d3bd3c9d9daddbb417914c Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Wed, 19 Feb 2025 22:42:55 +0200 Subject: [PATCH 16/59] dont clone liveness --- vyper/venom/passes/func_inliner.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index b1ef2e5560..5564b9e223 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -226,7 +226,6 @@ def _clone_instruction(self, inst: IRInstruction, prefix: str) -> IRInstruction: clone = IRInstruction(inst.opcode, ops, output) clone.parent = inst.parent - clone.liveness = inst.liveness.copy() clone.annotation = inst.annotation clone.ast_source = inst.ast_source clone.error_msg = inst.error_msg From 763e00f8956ffef3657a4fd9dc476844dccf2faf Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Thu, 20 Feb 2025 10:30:23 +0200 Subject: [PATCH 17/59] remove dummy line --- vyper/venom/ir_node_to_venom.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index cb4b1d623e..f774cb2648 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -198,7 +198,6 @@ def _handle_internal_func( ) -> IRFunction: global _alloca_table - fn = fn.ctx.create_function(ir.args[0].args[0].value) fn = fn.ctx.create_function(ir.args[0].args[0].value) bb = fn.get_basic_block() From b1b3bda8409b5aa3bbcaa0be96efabed6975335e Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 09:37:18 +0100 Subject: [PATCH 18/59] fix expected jumps --- tests/unit/compiler/test_source_map.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/unit/compiler/test_source_map.py b/tests/unit/compiler/test_source_map.py index 974e4f2ece..6f0b37317d 100644 --- a/tests/unit/compiler/test_source_map.py +++ b/tests/unit/compiler/test_source_map.py @@ -37,21 +37,22 @@ def test_jump_map(optimize, experimental_codegen): pos_map = source_map["pc_pos_map"] jump_map = source_map["pc_jump_map"] - expected_jumps = 1 - expected_internals = 2 if optimize == OptimizationLevel.NONE: # some jumps which don't get optimized out when optimizer is off # (slightly different behavior depending if venom pipeline is enabled): - if not experimental_codegen: - expected_jumps = 3 + if experimental_codegen: + expected_jumps = 0 + expected_internals = 0 else: - expected_jumps = 2 - else: - if not experimental_codegen: + expected_jumps = 3 expected_internals = 2 - else: + else: + if experimental_codegen: expected_jumps = 0 expected_internals = 0 + else: + expected_jumps = 1 + expected_internals = 2 assert len([v for v in jump_map.values() if v == "o"]) == expected_jumps assert len([v for v in jump_map.values() if v == "i"]) == expected_internals From 8c3da00bd35cfae1cbfef73bdcb8a2217ae72989 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 09:42:32 +0100 Subject: [PATCH 19/59] match params by index --- vyper/venom/passes/func_inliner.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index 466ad881b6..c8a7ecbdcc 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -129,20 +129,17 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: for bb in func_copy.get_basic_blocks(): bb.parent = call_site_func call_site_func.append_basic_block(bb) + param_idx = 0 for inst in bb.instructions: if inst.opcode == "param": - if inst.annotation == self._RETURN_BUFFER_ANNOTATION: - inst.opcode = "store" - inst.operands = [call_site.operands[1]] - elif inst.annotation == self._RETURN_PC_ANNOTATION: + if inst.annotation == self._RETURN_PC_ANNOTATION: inst.make_nop() else: - assert inst.annotation is not None - arg = func.get_param_by_name(inst.annotation) - assert arg is not None inst.opcode = "store" - inst.operands = [call_site.operands[arg.index + 1]] + inst.operands = [call_site.operands[-param_idx - 1]] inst.annotation = None + param_idx += 1 + elif inst.opcode == "palloca": inst.opcode = "store" inst.operands = [inst.operands[0]] From dd9a05c3a3cb41b4423783ded4e470cf363ad00c Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 09:44:04 +0100 Subject: [PATCH 20/59] remove liveness copy --- vyper/venom/passes/func_inliner.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index c8a7ecbdcc..2a0b803332 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -218,7 +218,6 @@ def _clone_instruction(self, inst: IRInstruction, prefix: str) -> IRInstruction: clone = IRInstruction(inst.opcode, ops, output) clone.parent = inst.parent - clone.liveness = inst.liveness.copy() clone.annotation = inst.annotation clone.ast_source = inst.ast_source clone.error_msg = inst.error_msg From 17d3409dad4da0e40b9477e8e482a61fd95c54b8 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 10:09:30 +0100 Subject: [PATCH 21/59] generalize param handling --- vyper/venom/passes/func_inliner.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index 2a0b803332..33c395e16a 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -110,7 +110,7 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: if call_site.opcode != "invoke": raise CompilerPanic(f"Expected invoke instruction, got {call_site.opcode}") - prefix = f"inline_{self.inline_count}_" + prefix = f"il{self.inline_count}_" self.inline_count += 1 call_site_bb = call_site.parent call_site_func = call_site_bb.parent @@ -132,24 +132,14 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: param_idx = 0 for inst in bb.instructions: if inst.opcode == "param": - if inst.annotation == self._RETURN_PC_ANNOTATION: - inst.make_nop() - else: - inst.opcode = "store" - inst.operands = [call_site.operands[-param_idx - 1]] - inst.annotation = None + inst.opcode = "store" + inst.operands = [call_site.operands[-param_idx - 1]] + inst.annotation = None param_idx += 1 elif inst.opcode == "palloca": inst.opcode = "store" inst.operands = [inst.operands[0]] - elif inst.opcode == "store": - assert inst.output is not None # mypy is not smart enough - if ( - self._RETURN_OFFSET_MARKER in inst.output.name - or self._RETURN_SIZE_MARKER in inst.output.name - ): - inst.make_nop() elif inst.opcode == "ret": inst.opcode = "jmp" inst.operands = [call_site_return.label] From 64d983f9949fe502cef0be609da8c2741c0cb753 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 10:20:56 +0100 Subject: [PATCH 22/59] add notes --- vyper/venom/passes/func_inliner.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index 33c395e16a..fd408a3899 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -132,9 +132,13 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: param_idx = 0 for inst in bb.instructions: if inst.opcode == "param": + # NOTE: one of these params is the return pc. technically assigning + # a variable to a label (e.g. %1 = @label) as we are doing here is + # not valid venom code, but it will get removed in store elimination + # (or unused variable elimination) inst.opcode = "store" - inst.operands = [call_site.operands[-param_idx - 1]] - inst.annotation = None + val = call_site.operands[-param_idx - 1] + inst.operands = [val] param_idx += 1 elif inst.opcode == "palloca": From 4343ca6eb03b5d66c9ed503b4ff676bc5cc98c74 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 10:21:16 +0100 Subject: [PATCH 23/59] remove magic variables --- vyper/venom/passes/func_inliner.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index fd408a3899..b4de9c1ece 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -31,11 +31,6 @@ class FuncInlinerPass(IRGlobalPass): - Invalidates DFG, CFG and VarEquivalence analyses """ - _RETURN_BUFFER_ANNOTATION = "return_buffer" - _RETURN_PC_ANNOTATION = "return_pc" - _RETURN_OFFSET_MARKER = "ret_ofst" - _RETURN_SIZE_MARKER = "ret_size" - inline_count: int fcg: FCGAnalysis From 817b67713def0981743b49fd3538bd2a44e0ecb7 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 10:25:37 +0100 Subject: [PATCH 24/59] perf: don't create new IRLiteral add notes --- vyper/venom/passes/func_inliner.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/func_inliner.py index b4de9c1ece..68f68fcb66 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/func_inliner.py @@ -135,7 +135,6 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: val = call_site.operands[-param_idx - 1] inst.operands = [val] param_idx += 1 - elif inst.opcode == "palloca": inst.opcode = "store" inst.operands = [inst.operands[0]] @@ -156,7 +155,7 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: def _build_call_walk(self, function: IRFunction) -> OrderedSet[IRFunction]: """ - DFS walk over the call graph. + postorder DFS walk over the call graph. """ visited = set() call_walk = [] @@ -195,11 +194,12 @@ def _clone_instruction(self, inst: IRInstruction, prefix: str) -> IRInstruction: ops: list[IROperand] = [] for op in inst.operands: if isinstance(op, IRLabel): + # label renaming is handled in inline_call_site ops.append(IRLabel(op.value)) elif isinstance(op, IRVariable): ops.append(IRVariable(f"{prefix}{op.name}")) else: - ops.append(IRLiteral(op.value)) + ops.append(op) output = None if inst.output: From 3e97de5e5c8b79e5217b863f0193188451379497 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 10:27:05 +0100 Subject: [PATCH 25/59] rename to function_inliner --- vyper/venom/__init__.py | 4 ++-- vyper/venom/passes/__init__.py | 2 +- vyper/venom/passes/{func_inliner.py => function_inliner.py} | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) rename vyper/venom/passes/{func_inliner.py => function_inliner.py} (99%) diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index 27cf85c119..cf69aea526 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -15,7 +15,7 @@ BranchOptimizationPass, DFTPass, FloatAllocas, - FuncInlinerPass, + FunctionInlinerPass, LoadElimination, LowerDloadPass, MakeSSA, @@ -96,7 +96,7 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel, ac: IRAnalysesCache def _run_global_passes(ctx: IRContext, optimize: OptimizationLevel, ir_analyses: dict) -> None: - FuncInlinerPass(ir_analyses, ctx).run_pass() + FunctionInlinerPass(ir_analyses, ctx).run_pass() def run_passes_on(ctx: IRContext, optimize: OptimizationLevel) -> None: diff --git a/vyper/venom/passes/__init__.py b/vyper/venom/passes/__init__.py index 489519b89b..c5e405242a 100644 --- a/vyper/venom/passes/__init__.py +++ b/vyper/venom/passes/__init__.py @@ -2,7 +2,7 @@ from .branch_optimization import BranchOptimizationPass from .dft import DFTPass from .float_allocas import FloatAllocas -from .func_inliner import FuncInlinerPass +from .function_inliner import FunctionInlinerPass from .literals_codesize import ReduceLiteralsCodesize from .load_elimination import LoadElimination from .lower_dload import LowerDloadPass diff --git a/vyper/venom/passes/func_inliner.py b/vyper/venom/passes/function_inliner.py similarity index 99% rename from vyper/venom/passes/func_inliner.py rename to vyper/venom/passes/function_inliner.py index 68f68fcb66..e4b8ef7532 100644 --- a/vyper/venom/passes/func_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -19,7 +19,7 @@ from vyper.venom.passes.base_pass import IRGlobalPass -class FuncInlinerPass(IRGlobalPass): +class FunctionInlinerPass(IRGlobalPass): """ This pass inlines functions into their call sites to reduce function call overhead. From b3443b0c05849a94c193281b7f830a8612a5e854 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 10:28:02 +0100 Subject: [PATCH 26/59] lint --- vyper/venom/passes/function_inliner.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index e4b8ef7532..4b57ee4d7b 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -6,14 +6,7 @@ from vyper.venom.analysis.cfg import CFGAnalysis from vyper.venom.analysis.dfg import DFGAnalysis from vyper.venom.analysis.fcg import FCGAnalysis -from vyper.venom.basicblock import ( - IRBasicBlock, - IRInstruction, - IRLabel, - IRLiteral, - IROperand, - IRVariable, -) +from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRLabel, IROperand, IRVariable from vyper.venom.function import IRFunction from vyper.venom.passes import FloatAllocas from vyper.venom.passes.base_pass import IRGlobalPass From 7d1721d69dd783dd3683def0420b296e1d5a6262 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 10:29:16 +0100 Subject: [PATCH 27/59] style --- vyper/venom/passes/function_inliner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index 4b57ee4d7b..6dc6080314 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -134,7 +134,7 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: elif inst.opcode == "ret": inst.opcode = "jmp" inst.operands = [call_site_return.label] - elif inst.opcode in ["jmp", "jnz", "djmp", "phi"]: + elif inst.opcode in ("jmp", "jnz", "djmp", "phi"): for i, label in enumerate(inst.operands): if isinstance(label, IRLabel) and func.has_basic_block(label.name): inst.operands[i] = IRLabel(f"{prefix}{label.name}") From 12f31da23e24e734e4d03accc0a68600d7fd92b1 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 10:34:39 +0100 Subject: [PATCH 28/59] refactor to use .entry_function --- tests/unit/compiler/venom/test_variable_equivalence.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/unit/compiler/venom/test_variable_equivalence.py b/tests/unit/compiler/venom/test_variable_equivalence.py index 717c026dff..a3e02b4715 100644 --- a/tests/unit/compiler/venom/test_variable_equivalence.py +++ b/tests/unit/compiler/venom/test_variable_equivalence.py @@ -6,11 +6,6 @@ from vyper.venom.context import IRContext -def _entry_fn(ctx: "IRContext"): - # TODO: make this part of IRContext - return next(iter(ctx.functions.values())) - - def test_variable_equivalence_dfg_order(): a_code = """ main: @@ -26,8 +21,8 @@ def test_variable_equivalence_dfg_order(): %2 = %1 %1 = 1 """ - fn1 = _entry_fn(parse_from_basic_block(a_code)) - fn2 = _entry_fn(parse_from_basic_block(b_code)) + fn1 = parse_from_basic_block(a_code).entry_function + fn2 = parse_from_basic_block(b_code).entry_function dfg1 = IRAnalysesCache(fn1).request_analysis(DFGAnalysis) dfg2 = IRAnalysesCache(fn2).request_analysis(DFGAnalysis) From 2d814958e977b711a777c42292af2b190fabd3a6 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 10:49:00 +0100 Subject: [PATCH 29/59] remove redundant loop --- vyper/venom/analysis/analysis.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/vyper/venom/analysis/analysis.py b/vyper/venom/analysis/analysis.py index 4cd573af8d..fcab3b4dd0 100644 --- a/vyper/venom/analysis/analysis.py +++ b/vyper/venom/analysis/analysis.py @@ -73,7 +73,4 @@ def force_analysis(self, analysis_cls: Type[IRAnalysis], *args, **kwargs): if analysis_cls in self.analyses_cache: self.invalidate_analysis(analysis_cls) - for analysis in self.analyses_cache.values(): - self.request_analysis(analysis.__class__) - return self.request_analysis(analysis_cls, *args, **kwargs) From 9c29301be5c08b5f9079f598b9813597a09a9e93 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 10:54:27 +0100 Subject: [PATCH 30/59] fix insert_instruction - only override source info if we are building venom --- vyper/venom/basicblock.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index 2039388c70..59a9074dd6 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -567,10 +567,11 @@ def insert_instruction(self, instruction: IRInstruction, index: Optional[int] = assert not self.is_terminated, (self, instruction) index = len(self.instructions) instruction.parent = self - if instruction.ast_source is None: - instruction.ast_source = self.parent.ast_source - if instruction.error_msg is None: - instruction.error_msg = self.parent.error_msg + fn = self.parent + if fn.ast_source is not None: + instruction.ast_source = fn.ast_source + if fn.error_msg is not None: + instruction.error_msg = fn.error_msg self.instructions.insert(index, instruction) def clear_nops(self) -> None: From 28c5c4f12cce273e5380ba233393c81c7076dc53 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 11:09:36 +0100 Subject: [PATCH 31/59] roll back optimization mode change --- vyper/compiler/phases.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index a5e17e1411..17812ee535 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -328,7 +328,7 @@ def generate_ir_nodes(global_ctx: ModuleT, settings: Settings) -> tuple[IRnode, with anchor_settings(settings): ir_nodes, ir_runtime = module.generate_ir_for_module(global_ctx) - if settings.optimize != OptimizationLevel.NONE and not settings.experimental_codegen: + if settings.optimize != OptimizationLevel.NONE: ir_nodes = optimizer.optimize(ir_nodes) ir_runtime = optimizer.optimize(ir_runtime) return ir_nodes, ir_runtime From b5e9410d01814da12b21fbd1a188e348f13319e2 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 11:09:54 +0100 Subject: [PATCH 32/59] small fixes/comments --- vyper/venom/__init__.py | 1 - vyper/venom/basicblock.py | 2 +- vyper/venom/context.py | 3 +-- vyper/venom/ir_node_to_venom.py | 10 +--------- vyper/venom/passes/load_elimination.py | 4 +--- vyper/venom/passes/make_ssa.py | 4 ++-- 6 files changed, 6 insertions(+), 18 deletions(-) diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index cf69aea526..67b1b492e4 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -84,7 +84,6 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel, ac: IRAnalysesCache AlgebraicOptimizationPass(ac, fn).run_pass() - # This improves the performance of cse RemoveUnusedVariablesPass(ac, fn).run_pass() StoreExpansionPass(ac, fn).run_pass() diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index 59a9074dd6..a253455cd9 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -585,11 +585,11 @@ def remove_instruction(self, instruction: IRInstruction) -> None: def remove_instructions_after(self, instruction: IRInstruction) -> None: assert isinstance(instruction, IRInstruction), "instruction must be an IRInstruction" assert instruction in self.instructions, "instruction must be in basic block" - # TODO: make sure this has coverage in the test suite self.instructions = self.instructions[: self.instructions.index(instruction) + 1] def ensure_well_formed(self): for inst in self.instructions: + assert inst.parent == self # sanity if inst.opcode == "revert": self.remove_instructions_after(inst) self.append_instruction("stop") # TODO: make revert a bb terminator? diff --git a/vyper/venom/context.py b/vyper/venom/context.py index ce08125b74..256f7d5f62 100644 --- a/vyper/venom/context.py +++ b/vyper/venom/context.py @@ -62,8 +62,7 @@ def remove_function(self, fn: IRFunction) -> None: def create_function(self, name: str) -> IRFunction: label = IRLabel(name, True) - if label in self.functions: - return self.functions[label] + assert label not in self.functions, f"duplicate function {label}" fn = IRFunction(label, self) fn.append_basic_block(IRBasicBlock(label, fn)) self.add_function(fn) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index f774cb2648..948d8e1fff 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -63,7 +63,7 @@ "gasprice", "gaslimit", "returndatasize", - # "mload", + "mload", "iload", "istore", "dload", @@ -418,9 +418,6 @@ def _convert_ir_bb(fn, ir, symbols): code = ir.args[2] _convert_ir_bb(fn, code, symbols) elif ir.value == "exit_to": - # TODO: cleanup - global _current_func_t - args = _convert_ir_bb_list(fn, ir.args[1:], symbols) var_list = args _append_return_args(fn, *var_list) @@ -444,11 +441,6 @@ def _convert_ir_bb(fn, ir, symbols): val, ptr = _convert_ir_bb_list(fn, reversed(ir.args), symbols) return fn.get_basic_block().append_instruction("mstore", val, ptr) - elif ir.value == "mload": - arg = ir.args[0] - ptr = _convert_ir_bb(fn, arg, symbols) - - return fn.get_basic_block().append_instruction("mload", ptr) elif ir.value == "ceil32": x = ir.args[0] expanded = IRnode.from_list(["and", ["add", x, 31], ["not", 31]]) diff --git a/vyper/venom/passes/load_elimination.py b/vyper/venom/passes/load_elimination.py index 685896053c..4575a1d93f 100644 --- a/vyper/venom/passes/load_elimination.py +++ b/vyper/venom/passes/load_elimination.py @@ -24,8 +24,6 @@ class LoadElimination(IRPass): # should this be renamed to EffectsElimination? def run_pass(self): - self.dfg = self.analyses_cache.request_analysis(DFGAnalysis) - for bb in self.function.get_basic_blocks(): self._process_bb(bb, Effects.MEMORY, "mload", "mstore") self._process_bb(bb, Effects.TRANSIENT, "tload", "tstore") @@ -37,7 +35,7 @@ def run_pass(self): self.analyses_cache.invalidate_analysis(DFGAnalysis) def equivalent(self, op1, op2): - return op1 == op2 or self.dfg.are_equivalent(op1, op2) + return op1 == op2 def get_literal(self, op): if isinstance(op, IRLiteral): diff --git a/vyper/venom/passes/make_ssa.py b/vyper/venom/passes/make_ssa.py index 043160d0a2..d2a0ab4408 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_post_walk} - has_already = {var: 0 for var in self.dom.dfs_post_walk} + work = {bb: 0 for bb in self.dom.dfs_post_walk} + has_already = {bb: 0 for bb in self.dom.dfs_post_walk} i = 0 # Iterate over all variables From bcf6f05173a10acf2091c3fb634c8d5e839f2f76 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 11:31:58 +0100 Subject: [PATCH 33/59] rename all_nonempty to all2 --- vyper/utils.py | 15 +++++++++------ vyper/venom/passes/mem2var.py | 6 +++--- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/vyper/utils.py b/vyper/utils.py index f1d84d5fd4..a73cc163cb 100644 --- a/vyper/utils.py +++ b/vyper/utils.py @@ -695,12 +695,15 @@ def safe_relpath(path): return path -def all_nonempty(iter): +def all2(iterator): """ This function checks if all elements in the given `iterable` are truthy, - similar to Python's built-in `all()` function. However, `all_nonempty` - diverges by returning `False` if the iterable is empty, whereas `all()` - would return `True` for an empty iterable. + similar to Python's built-in `all()` function. However, `all2` differs + in the case where there are no elements in the iterable. `all()` returns + `True` for the empty iterable, but `all2()` returns False. """ - items = list(iter) - return len(items) > 0 and all(items) + try: + s = next(iterator) + except StopIteration: + return False + return bool(s) and all(iterator) diff --git a/vyper/venom/passes/mem2var.py b/vyper/venom/passes/mem2var.py index 8b1fb1f0f8..af9da7ec08 100644 --- a/vyper/venom/passes/mem2var.py +++ b/vyper/venom/passes/mem2var.py @@ -1,4 +1,4 @@ -from vyper.utils import all_nonempty +from vyper.utils import all2 from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, LivenessAnalysis from vyper.venom.basicblock import IRInstruction, IRVariable from vyper.venom.function import IRFunction @@ -40,7 +40,7 @@ def _process_alloca_var(self, dfg: DFGAnalysis, alloca_inst, var: IRVariable): Otherwise, it is left as is. """ uses = dfg.get_uses(var) - if not all_nonempty(inst.opcode in ["mstore", "mload", "return"] for inst in uses): + if not all2(inst.opcode in ["mstore", "mload", "return"] for inst in uses): return alloca_id = alloca_inst.operands[2] @@ -67,7 +67,7 @@ def _process_palloca_var(self, dfg: DFGAnalysis, palloca_inst: IRInstruction, va instructions, it is promoted to a stack variable. Otherwise, it is left as is. """ uses = dfg.get_uses(var) - if not all_nonempty(inst.opcode in ["mstore", "mload"] for inst in uses): + if not all2(inst.opcode in ["mstore", "mload"] for inst in uses): return ofst, _size, alloca_id = palloca_inst.operands From 08113d72a6641db017a9a44df0e9a1253dbfd4b7 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 11:32:15 +0100 Subject: [PATCH 34/59] fix lint --- tests/unit/compiler/venom/test_variable_equivalence.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/compiler/venom/test_variable_equivalence.py b/tests/unit/compiler/venom/test_variable_equivalence.py index a3e02b4715..50732ddda9 100644 --- a/tests/unit/compiler/venom/test_variable_equivalence.py +++ b/tests/unit/compiler/venom/test_variable_equivalence.py @@ -3,7 +3,6 @@ from tests.venom_utils import parse_from_basic_block from vyper.venom.analysis import DFGAnalysis, IRAnalysesCache from vyper.venom.basicblock import IRVariable -from vyper.venom.context import IRContext def test_variable_equivalence_dfg_order(): From 9fc737a2bd035f84f8ac876f0b30eb3a2fa7fb21 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 11:35:09 +0100 Subject: [PATCH 35/59] move a test file --- .../codegen/calling_convention/test_inlineable_functions.py} | 3 +++ 1 file changed, 3 insertions(+) rename tests/{unit/compiler/venom/test_inliner.py => functional/codegen/calling_convention/test_inlineable_functions.py} (91%) diff --git a/tests/unit/compiler/venom/test_inliner.py b/tests/functional/codegen/calling_convention/test_inlineable_functions.py similarity index 91% rename from tests/unit/compiler/venom/test_inliner.py rename to tests/functional/codegen/calling_convention/test_inlineable_functions.py index 9b6690a15e..7ac5931de1 100644 --- a/tests/unit/compiler/venom/test_inliner.py +++ b/tests/functional/codegen/calling_convention/test_inlineable_functions.py @@ -2,6 +2,9 @@ from tests.evm_backends.base_env import ExecutionReverted +# note for refactor: this may be able to be merged with +# calling_convention/test_internal_call.py + def test_call_in_call(get_contract): code = """ From 0ac0533b548e490068c3e7375487b4a4142f331f Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 11:36:06 +0100 Subject: [PATCH 36/59] use tx_failed fixture --- .../calling_convention/test_inlineable_functions.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/functional/codegen/calling_convention/test_inlineable_functions.py b/tests/functional/codegen/calling_convention/test_inlineable_functions.py index 7ac5931de1..1158650e42 100644 --- a/tests/functional/codegen/calling_convention/test_inlineable_functions.py +++ b/tests/functional/codegen/calling_convention/test_inlineable_functions.py @@ -2,6 +2,9 @@ from tests.evm_backends.base_env import ExecutionReverted +""" +Test functionality of internal functions which may be inlined +""" # note for refactor: this may be able to be merged with # calling_convention/test_internal_call.py @@ -25,7 +28,7 @@ def foo() -> uint256: assert c.foo() == 5 -def test_call_in_call_with_raise(get_contract): +def test_call_in_call_with_raise(get_contract, tx_failed): code = """ @internal def sum(a: uint256) -> uint256: @@ -46,7 +49,7 @@ def test(a: uint256) -> uint256: assert c.test(2) == 3 - with pytest.raises(ExecutionReverted): + with tx_failed(): c.test(0) From 63056bd5c82d35805aee0e51776acce3f749a2f6 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 11:37:20 +0100 Subject: [PATCH 37/59] remove stray newline --- vyper/venom/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index 67b1b492e4..7df3cf3ee7 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -83,7 +83,6 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel, ac: IRAnalysesCache BranchOptimizationPass(ac, fn).run_pass() AlgebraicOptimizationPass(ac, fn).run_pass() - RemoveUnusedVariablesPass(ac, fn).run_pass() StoreExpansionPass(ac, fn).run_pass() From 603e9a018069b3f5e04556b0633ac426f07a8fae Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 11:48:33 +0100 Subject: [PATCH 38/59] add notes --- vyper/venom/analysis/equivalent_vars.py | 29 ------------------------- vyper/venom/passes/function_inliner.py | 6 +++-- 2 files changed, 4 insertions(+), 31 deletions(-) delete mode 100644 vyper/venom/analysis/equivalent_vars.py diff --git a/vyper/venom/analysis/equivalent_vars.py b/vyper/venom/analysis/equivalent_vars.py deleted file mode 100644 index 8a333fa401..0000000000 --- a/vyper/venom/analysis/equivalent_vars.py +++ /dev/null @@ -1,29 +0,0 @@ -from vyper.venom.analysis import DFGAnalysis, IRAnalysis - - -class VarEquivalenceAnalysis(IRAnalysis): - """ - Generate equivalence sets of variables. Essentially, variables chained - by store instructions are equivalent. These are used to avoid swapping - variables which are the same during venom_to_assembly, and are produced - by the StoreExpansionPass. - """ - - def analyze(self): - dfg = self.analyses_cache.request_analysis(DFGAnalysis) - - self._equivalence_set = {} - - for output, inst in dfg.outputs.items(): - if inst.opcode != "store": - continue - - self._equivalence_set[output] = self._get_equivalent(inst.operands[0]) - - def _get_equivalent(self, var): - if var in self._equivalence_set: - return self._equivalence_set[var] - return var - - def equivalent(self, var1, var2): - return self._get_equivalent(var1) == self._get_equivalent(var2) diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index 6dc6080314..c30fd83b8a 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -21,7 +21,7 @@ class FunctionInlinerPass(IRGlobalPass): Side effects: - Modifies the control flow graph - - Invalidates DFG, CFG and VarEquivalence analyses + - Invalidates DFG and CFG """ inline_count: int @@ -47,6 +47,8 @@ def run_pass(self): self.ctx.remove_function(candidate) self.walk.remove(candidate) + # TODO: check if recomputing this is a perf issue or we should rather + # update it in-place. self.fcg = self.analyses_caches[entry].force_analysis(FCGAnalysis) def _select_inline_candidate(self) -> Optional[IRFunction]: @@ -67,7 +69,7 @@ def _select_inline_candidate(self) -> Optional[IRFunction]: return func elif self.settings.optimize == OptimizationLevel.NONE: continue - else: + else: # pragma: nocover raise CompilerPanic( f"Unsupported inlining optimization level: {self.settings.optimize}" ) From 827e729e4e7d500dbe6f702f574b29feace2e07f Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 13:24:16 +0100 Subject: [PATCH 39/59] add review comment --- vyper/venom/passes/function_inliner.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index c30fd83b8a..e88da39c10 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -138,6 +138,7 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: inst.operands = [call_site_return.label] elif inst.opcode in ("jmp", "jnz", "djmp", "phi"): for i, label in enumerate(inst.operands): + # REVIEW: is has_basic_block necessary? if isinstance(label, IRLabel) and func.has_basic_block(label.name): inst.operands[i] = IRLabel(f"{prefix}{label.name}") elif inst.opcode == "revert": From 58a6ed51edc4d72578fe176d301bbc3a6ddebf7c Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 13:26:44 +0100 Subject: [PATCH 40/59] add note --- vyper/venom/ir_node_to_venom.py | 1 + 1 file changed, 1 insertion(+) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 948d8e1fff..accbafd2cf 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -420,6 +420,7 @@ def _convert_ir_bb(fn, ir, symbols): elif ir.value == "exit_to": args = _convert_ir_bb_list(fn, ir.args[1:], symbols) var_list = args + # TODO: only append return args if the function is external _append_return_args(fn, *var_list) bb = fn.get_basic_block() if bb.is_terminated: From 82e144ff2a36a02041a5fea541fa5d54c55620d4 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 14:49:16 +0100 Subject: [PATCH 41/59] add assert back --- vyper/venom/function.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vyper/venom/function.py b/vyper/venom/function.py index 63301a8e14..d2148dee05 100644 --- a/vyper/venom/function.py +++ b/vyper/venom/function.py @@ -54,7 +54,7 @@ def append_basic_block(self, bb: IRBasicBlock): Append basic block to function. """ assert isinstance(bb, IRBasicBlock), bb - # assert bb.label.name not in self._basic_block_dict, bb.label + assert bb.label.name not in self._basic_block_dict, bb.label self._basic_block_dict[bb.label.name] = bb def remove_basic_block(self, bb: IRBasicBlock): From 96d1023b8886817f4001240a210c76a25a34598b Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 15:02:16 +0100 Subject: [PATCH 42/59] small fixes --- vyper/venom/context.py | 1 - vyper/venom/passes/function_inliner.py | 3 +++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/vyper/venom/context.py b/vyper/venom/context.py index 256f7d5f62..55be7d2f61 100644 --- a/vyper/venom/context.py +++ b/vyper/venom/context.py @@ -64,7 +64,6 @@ def create_function(self, name: str) -> IRFunction: label = IRLabel(name, True) assert label not in self.functions, f"duplicate function {label}" fn = IRFunction(label, self) - fn.append_basic_block(IRBasicBlock(label, fn)) self.add_function(fn) return fn diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index e88da39c10..5f0d077656 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -174,6 +174,9 @@ def dfs(fn): def _clone_function(self, func: IRFunction, prefix: str) -> IRFunction: new_func_label = IRLabel(f"{prefix}{func.name.value}") clone = IRFunction(new_func_label) + # clear the bb that is added by default + # consider using func.copy() intead? + clone._basic_block_dict.clear() for bb in func.get_basic_blocks(): clone.append_basic_block(self._clone_basic_block(bb, prefix)) return clone From c217b720c86dff5650c57cc2d2f67de38583b42d Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 15:25:15 +0100 Subject: [PATCH 43/59] refactor settings threading --- vyper/compiler/phases.py | 4 ++-- vyper/venom/__init__.py | 7 ++++--- vyper/venom/analysis/__init__.py | 1 + vyper/venom/passes/base_pass.py | 12 ++---------- vyper/venom/passes/function_inliner.py | 26 +++++++++++++++++--------- 5 files changed, 26 insertions(+), 24 deletions(-) diff --git a/vyper/compiler/phases.py b/vyper/compiler/phases.py index 17812ee535..cdbe9f83b3 100644 --- a/vyper/compiler/phases.py +++ b/vyper/compiler/phases.py @@ -256,8 +256,8 @@ def function_signatures(self) -> dict[str, ContractFunctionT]: @cached_property def venom_functions(self): deploy_ir, runtime_ir = self._ir_output - deploy_venom = generate_ir(deploy_ir, self.settings.optimize) - runtime_venom = generate_ir(runtime_ir, self.settings.optimize) + deploy_venom = generate_ir(deploy_ir, self.settings) + runtime_venom = generate_ir(runtime_ir, self.settings) return deploy_venom, runtime_venom @cached_property diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index 7df3cf3ee7..3601de71b2 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -4,7 +4,7 @@ from typing import Optional from vyper.codegen.ir_node import IRnode -from vyper.compiler.settings import OptimizationLevel +from vyper.compiler.settings import OptimizationLevel, Settings from vyper.venom.analysis.analysis import IRAnalysesCache from vyper.venom.context import IRContext from vyper.venom.function import IRFunction @@ -94,7 +94,7 @@ def _run_passes(fn: IRFunction, optimize: OptimizationLevel, ac: IRAnalysesCache def _run_global_passes(ctx: IRContext, optimize: OptimizationLevel, ir_analyses: dict) -> None: - FunctionInlinerPass(ir_analyses, ctx).run_pass() + FunctionInlinerPass(ir_analyses, ctx, optimize).run_pass() def run_passes_on(ctx: IRContext, optimize: OptimizationLevel) -> None: @@ -112,10 +112,11 @@ def run_passes_on(ctx: IRContext, optimize: OptimizationLevel) -> None: _run_passes(fn, optimize, ir_analyses[fn]) -def generate_ir(ir: IRnode, optimize: OptimizationLevel) -> IRContext: +def generate_ir(ir: IRnode, settings: Settings) -> IRContext: # Convert "old" IR to "new" IR ctx = ir_node_to_venom(ir) + optimize = settings.optimize run_passes_on(ctx, optimize) return ctx diff --git a/vyper/venom/analysis/__init__.py b/vyper/venom/analysis/__init__.py index fd6437b431..c7cbb3dec8 100644 --- a/vyper/venom/analysis/__init__.py +++ b/vyper/venom/analysis/__init__.py @@ -2,4 +2,5 @@ from .cfg import CFGAnalysis from .dfg import DFGAnalysis from .dominators import DominatorTreeAnalysis +from .fcg import FCGAnalysis from .liveness import LivenessAnalysis diff --git a/vyper/venom/passes/base_pass.py b/vyper/venom/passes/base_pass.py index 2999d3a4b1..5d3807606f 100644 --- a/vyper/venom/passes/base_pass.py +++ b/vyper/venom/passes/base_pass.py @@ -29,18 +29,10 @@ class IRGlobalPass: ctx: IRContext analyses_caches: dict[IRFunction, IRAnalysesCache] - settings: Settings - - def __init__( - self, - analyses_caches: dict[IRFunction, IRAnalysesCache], - ctx: IRContext, - settings: Optional[Settings] = None, - ): + + def __init__(self, analyses_caches: dict[IRFunction, IRAnalysesCache], ctx: IRContext): self.analyses_caches = analyses_caches self.ctx = ctx - settings = settings or get_global_settings() - self.settings = settings or Settings() def run_pass(self, *args, **kwargs): raise NotImplementedError(f"Not implemented! {self.__class__}.run_pass()") diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index 5f0d077656..e3b041887b 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -3,10 +3,9 @@ from vyper.compiler.settings import OptimizationLevel from vyper.exceptions import CompilerPanic from vyper.utils import OrderedSet -from vyper.venom.analysis.cfg import CFGAnalysis -from vyper.venom.analysis.dfg import DFGAnalysis -from vyper.venom.analysis.fcg import FCGAnalysis +from vyper.venom.analysis import CFGAnalysis, DFGAnalysis, FCGAnalysis, IRAnalysesCache from vyper.venom.basicblock import IRBasicBlock, IRInstruction, IRLabel, IROperand, IRVariable +from vyper.venom.context import IRContext from vyper.venom.function import IRFunction from vyper.venom.passes import FloatAllocas from vyper.venom.passes.base_pass import IRGlobalPass @@ -26,6 +25,17 @@ class FunctionInlinerPass(IRGlobalPass): inline_count: int fcg: FCGAnalysis + optimize: OptimizationLevel + + def __init__( + self, + analyses_caches: dict[IRFunction, IRAnalysesCache], + ctx: IRContext, + optimize: OptimizationLevel, + ): + self.analyses_caches = analyses_caches + self.ctx = ctx + self.optimize = optimize def run_pass(self): entry = self.ctx.entry_function @@ -62,17 +72,15 @@ def _select_inline_candidate(self) -> Optional[IRFunction]: return func # Decide whether to inline based on the optimization level. - if self.settings.optimize == OptimizationLevel.CODESIZE: + if self.optimize == OptimizationLevel.CODESIZE: continue - elif self.settings.optimize == OptimizationLevel.GAS: + elif self.optimize == OptimizationLevel.GAS: if func.code_size_cost <= 15: return func - elif self.settings.optimize == OptimizationLevel.NONE: + elif self.optimize == OptimizationLevel.NONE: continue else: # pragma: nocover - raise CompilerPanic( - f"Unsupported inlining optimization level: {self.settings.optimize}" - ) + raise CompilerPanic(f"Unsupported inlining optimization level: {self.optimize}") return None From 290ff9e58c37d2bdae8706b4b100636a98576357 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 15:26:19 +0100 Subject: [PATCH 44/59] fix lint --- .../codegen/calling_convention/test_inlineable_functions.py | 4 ---- vyper/venom/__init__.py | 1 + vyper/venom/passes/base_pass.py | 3 --- 3 files changed, 1 insertion(+), 7 deletions(-) diff --git a/tests/functional/codegen/calling_convention/test_inlineable_functions.py b/tests/functional/codegen/calling_convention/test_inlineable_functions.py index 1158650e42..eae8a84bcd 100644 --- a/tests/functional/codegen/calling_convention/test_inlineable_functions.py +++ b/tests/functional/codegen/calling_convention/test_inlineable_functions.py @@ -1,7 +1,3 @@ -import pytest - -from tests.evm_backends.base_env import ExecutionReverted - """ Test functionality of internal functions which may be inlined """ diff --git a/vyper/venom/__init__.py b/vyper/venom/__init__.py index 3601de71b2..af9a39683e 100644 --- a/vyper/venom/__init__.py +++ b/vyper/venom/__init__.py @@ -117,6 +117,7 @@ def generate_ir(ir: IRnode, settings: Settings) -> IRContext: ctx = ir_node_to_venom(ir) optimize = settings.optimize + assert optimize is not None # help mypy run_passes_on(ctx, optimize) return ctx diff --git a/vyper/venom/passes/base_pass.py b/vyper/venom/passes/base_pass.py index 5d3807606f..eb8287e93b 100644 --- a/vyper/venom/passes/base_pass.py +++ b/vyper/venom/passes/base_pass.py @@ -1,6 +1,3 @@ -from typing import Optional - -from vyper.compiler.settings import Settings, get_global_settings from vyper.venom.analysis import IRAnalysesCache from vyper.venom.context import IRContext from vyper.venom.function import IRFunction From cc192db700e71f6726c9852d3dacec32e67452b5 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 15:48:50 +0100 Subject: [PATCH 45/59] generalize label renaming --- vyper/venom/passes/function_inliner.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index e3b041887b..73feca490e 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -144,11 +144,6 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: elif inst.opcode == "ret": inst.opcode = "jmp" inst.operands = [call_site_return.label] - elif inst.opcode in ("jmp", "jnz", "djmp", "phi"): - for i, label in enumerate(inst.operands): - # REVIEW: is has_basic_block necessary? - if isinstance(label, IRLabel) and func.has_basic_block(label.name): - inst.operands[i] = IRLabel(f"{prefix}{label.name}") elif inst.opcode == "revert": bb.remove_instructions_after(inst) bb.append_instruction("stop") @@ -198,11 +193,17 @@ def _clone_basic_block(self, bb: IRBasicBlock, prefix: str) -> IRBasicBlock: return new_bb def _clone_instruction(self, inst: IRInstruction, prefix: str) -> IRInstruction: + func = inst.parent.parent ops: list[IROperand] = [] for op in inst.operands: if isinstance(op, IRLabel): - # label renaming is handled in inline_call_site - ops.append(IRLabel(op.value)) + if func.has_basic_block(op.name): + # it is a valid label inside of this function + label = IRLabel(f"{prefix}{op.name}") + else: + # otherwise it is something else (like a data label) + label = op + ops.append(label) elif isinstance(op, IRVariable): ops.append(IRVariable(f"{prefix}{op.name}")) else: From a8702756bcd9a5ba7d602f386fa95501a9bf029f Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 16:00:17 +0100 Subject: [PATCH 46/59] fix lint --- vyper/venom/passes/function_inliner.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index 73feca490e..77821afac3 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -33,8 +33,7 @@ def __init__( ctx: IRContext, optimize: OptimizationLevel, ): - self.analyses_caches = analyses_caches - self.ctx = ctx + super().__init__(analyses_caches, ctx) self.optimize = optimize def run_pass(self): From 34b2023e8e45d79fef017fabe591631a110f4644 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Thu, 20 Feb 2025 22:59:23 +0100 Subject: [PATCH 47/59] revert grammar change --- vyper/venom/basicblock.py | 4 ++++ vyper/venom/parser.py | 2 +- vyper/venom/passes/function_inliner.py | 4 ++-- vyper/venom/passes/mem2var.py | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index a253455cd9..4f61dae2d1 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -194,6 +194,10 @@ def __init__(self, name: str, version: int = 0) -> None: def name(self) -> str: return self._name + @property + def plain_name(self) -> str: + return self.name.strip("%") + class IRLabel(IROperand): """ diff --git a/vyper/venom/parser.py b/vyper/venom/parser.py index 2b47b2e335..91c030826c 100644 --- a/vyper/venom/parser.py +++ b/vyper/venom/parser.py @@ -49,7 +49,7 @@ CONST: SIGNED_INT OPCODE: CNAME - VAR_IDENT: "%" (DIGIT|LETTER|"_"|":"|"%"|"@")+ + VAR_IDENT: "%" (DIGIT|LETTER|"_"|":")+ # handy for identifier to be an escaped string sometimes # (especially for machine-generated labels) diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index 77821afac3..12c6421c57 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -204,13 +204,13 @@ def _clone_instruction(self, inst: IRInstruction, prefix: str) -> IRInstruction: label = op ops.append(label) elif isinstance(op, IRVariable): - ops.append(IRVariable(f"{prefix}{op.name}")) + ops.append(IRVariable(f"{prefix}{op.plain_name}")) else: ops.append(op) output = None if inst.output: - output = IRVariable(f"{prefix}{inst.output.name}") + output = IRVariable(f"{prefix}{inst.output.plain_name}") clone = IRInstruction(inst.opcode, ops, output) clone.parent = inst.parent diff --git a/vyper/venom/passes/mem2var.py b/vyper/venom/passes/mem2var.py index af9da7ec08..1ecf25c468 100644 --- a/vyper/venom/passes/mem2var.py +++ b/vyper/venom/passes/mem2var.py @@ -29,7 +29,7 @@ def run_pass(self): def _mk_varname(self, varname: str, alloca_id: int): varname = varname.removeprefix("%") - varname = f"alloca_{alloca_id}_@{varname}_{self.var_name_count}" + varname = f"alloca_{alloca_id}_{varname}_{self.var_name_count}" self.var_name_count += 1 return varname From 511283f76fddd6d4d261247de5fb1607b74fffe4 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Fri, 21 Feb 2025 09:24:38 +0100 Subject: [PATCH 48/59] update inlined variable names, add annotation --- vyper/venom/passes/function_inliner.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index 12c6421c57..616c74c039 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -107,7 +107,7 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: if call_site.opcode != "invoke": raise CompilerPanic(f"Expected invoke instruction, got {call_site.opcode}") - prefix = f"il{self.inline_count}_" + prefix = f"inl{self.inline_count}_" self.inline_count += 1 call_site_bb = call_site.parent call_site_func = call_site_bb.parent @@ -148,6 +148,10 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: bb.append_instruction("stop") break + for inst in bb.instructions: + if not inst.annotation: + inst.annotation = f"from {func.name}" + call_site_bb.instructions = call_site_bb.instructions[:call_idx] call_site_bb.append_instruction("jmp", func_copy.entry.label) From 3b15a33354eefc4f30defb8b7fabedadbd057a73 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Fri, 21 Feb 2025 10:59:24 +0200 Subject: [PATCH 49/59] readability improvement for annotations --- vyper/venom/basicblock.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/vyper/venom/basicblock.py b/vyper/venom/basicblock.py index 4f61dae2d1..19e24e5a1e 100644 --- a/vyper/venom/basicblock.py +++ b/vyper/venom/basicblock.py @@ -439,7 +439,7 @@ def __repr__(self) -> str: s += ", ".join([(f"@{op}" if isinstance(op, IRLabel) else str(op)) for op in operands]) if self.annotation: - s += f" ; {self.annotation}" + s = f"{s: <30} ; {self.annotation}" # debug: # if self.error_msg: From 7b2ee89d0ddeb20b62951318ce3d8f5d4c42d628 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Fri, 21 Feb 2025 12:37:49 +0200 Subject: [PATCH 50/59] merge fixup --- vyper/venom/ir_node_to_venom.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index f6ea82cd0b..17d3bf58e2 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -292,9 +292,6 @@ def _handle_internal_func( symbols[arg.name] = ret arg.func_var = ret - assert buf is not None # help mypy - symbols["return_buffer"] = buf - # return address return_pc = bb.append_instruction("param") assert return_pc is not None # help mypy From 52e12dd38a41e9bc8fa49fcbbf37b8b8cf145009 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 22 Feb 2025 11:31:40 +0100 Subject: [PATCH 51/59] add a note --- vyper/venom/ir_node_to_venom.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 17d3bf58e2..8b8af426d9 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -203,6 +203,7 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio if not _is_word_type(alloca.typ): continue ptr = _alloca_table[alloca._id] + # to be optimized out by mem2var stack_arg = bb.append_instruction("mload", ptr) assert stack_arg is not None stack_args.append(stack_arg) @@ -212,6 +213,7 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio ret_value = bb.append_invoke_instruction(ret_args, returns=True) # type: ignore assert ret_value is not None assert isinstance(return_buf, IROperand) + # to be optimized out by mem2var bb.append_instruction("mstore", ret_value, return_buf) return return_buf From ea2967dd0a0d01d1f8ddc273728c66ded41be0c4 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 22 Feb 2025 11:37:37 +0100 Subject: [PATCH 52/59] fix regression --- vyper/venom/ir_node_to_venom.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 8b8af426d9..eb6a84d301 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -678,7 +678,10 @@ def emit_body_blocks(): assert alloca._callsite is not None if alloca._id not in _alloca_table: bb = fn.get_basic_block() - ptr = IRLiteral(alloca.offset) + if _is_word_type(alloca.typ): + ptr = bb.append_instruction("alloca", alloca.offset, alloca.size, alloca._id) + else: + ptr = IRLiteral(alloca.offset) _alloca_table[alloca._id] = ptr ret = _alloca_table[alloca._id] From ff49ca635cc29bd9d61cda1d74cf47597cc25032 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 22 Feb 2025 12:21:34 +0100 Subject: [PATCH 53/59] fix bad alignment of stack args --- vyper/venom/ir_node_to_venom.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index eb6a84d301..22437bad37 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -192,9 +192,6 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio if len(converted_args) > 1: return_buf = converted_args[0] - if return_buf is not None: - if not returns_word: - ret_args.append(return_buf) # type: ignore callsite_args = _callsites[callsite] stack_args = [] @@ -209,6 +206,10 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio stack_args.append(stack_arg) ret_args.extend(stack_args) + if return_buf is not None: + if not returns_word: + ret_args.append(return_buf) # type: ignore + if returns_word: ret_value = bb.append_invoke_instruction(ret_args, returns=True) # type: ignore assert ret_value is not None From 5e5e2dbc2812fa36141863487672c58a86d5655c Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 22 Feb 2025 12:24:10 +0100 Subject: [PATCH 54/59] improve mem2var annotations --- vyper/venom/passes/mem2var.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/vyper/venom/passes/mem2var.py b/vyper/venom/passes/mem2var.py index 1ecf25c468..1034213a8d 100644 --- a/vyper/venom/passes/mem2var.py +++ b/vyper/venom/passes/mem2var.py @@ -61,6 +61,10 @@ def _process_alloca_var(self, dfg: DFGAnalysis, alloca_inst, var: IRVariable): new_inst = IRInstruction("mstore", [var, inst.operands[1]]) bb.insert_instruction(new_inst, idx) + inst.annotation = f"mem2var alloca{alloca_id}" + if alloca_inst.annotation is not None: + inst.annotation += f" ({alloca_inst.annotation})" + def _process_palloca_var(self, dfg: DFGAnalysis, palloca_inst: IRInstruction, var: IRVariable): """ Process alloca allocated variable. If it is only used by mstore/mload From 3b190b242ebdc52035fb5cd407c9cc85c05f1603 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 22 Feb 2025 12:24:42 +0100 Subject: [PATCH 55/59] add a sanity check --- vyper/venom/passes/function_inliner.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/vyper/venom/passes/function_inliner.py b/vyper/venom/passes/function_inliner.py index 33faaa0068..338f9cfb91 100644 --- a/vyper/venom/passes/function_inliner.py +++ b/vyper/venom/passes/function_inliner.py @@ -123,6 +123,8 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: func_copy = self._clone_function(func, prefix) + call_site_args = call_site.operands.copy() + for bb in func_copy.get_basic_blocks(): bb.parent = call_site_func call_site_func.append_basic_block(bb) @@ -134,7 +136,7 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: # not valid venom code, but it will get removed in store elimination # (or unused variable elimination) inst.opcode = "store" - val = call_site.operands[-param_idx - 1] + val = call_site_args.pop() inst.operands = [val] param_idx += 1 elif inst.opcode == "palloca": @@ -157,6 +159,8 @@ def _inline_call_site(self, func: IRFunction, call_site: IRInstruction) -> None: if not inst.annotation: inst.annotation = f"from {func.name}" + assert len(call_site_args) == 0 + call_site_bb.instructions = call_site_bb.instructions[:call_idx] call_site_bb.append_instruction("jmp", func_copy.entry.label) From b95cba00af78ab40926e8dfa8ff83dab5a3f581a Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 22 Feb 2025 12:25:43 +0100 Subject: [PATCH 56/59] update generated alloca id --- vyper/venom/ir_node_to_venom.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 22437bad37..d360f205cf 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -280,7 +280,8 @@ def _handle_internal_func( # this alloca should be stripped by mem2var. we can remove # the hardcoded offset once we have proper memory allocator # functionality in venom. - buf = bb.append_instruction("alloca", IRLiteral(-1), IRLiteral(-1), IRLiteral(-1)) + # (note frontend generates alloca IDs starting from 1) + buf = bb.append_instruction("alloca", IRLiteral(-1), IRLiteral(-1), IRLiteral(0)) else: buf = bb.append_instruction("param") bb.instructions[-1].annotation = "return_buffer" From 67c59d2b021f0b3aa62d4571ac5c7eb9dd71241c Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 22 Feb 2025 12:30:15 +0100 Subject: [PATCH 57/59] fix lint, small cleanup --- vyper/venom/ir_node_to_venom.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index d360f205cf..c5912b34e6 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -168,14 +168,14 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio global _callsites setup_ir = ir.args[1] goto_ir = [ir for ir in ir.args if ir.value == "goto"][0] - target_label = goto_ir.args[0].value # goto - ret_args: list[IROperand] = [IRLabel(target_label)] # type: ignore + target_label = goto_ir.args[0].value # target function + assert isinstance(target_label, str) # help mypy func_t = ir.passthrough_metadata["func_t"] assert func_t is not None, "func_t not found in passthrough metadata" returns_word = _returns_word(func_t) - stack_args: list[IROperand] = [] + stack_args: list[IROperand] = [IRLabel(target_label)] if setup_ir != goto_ir: _convert_ir_bb(fn, setup_ir, symbols) @@ -192,9 +192,7 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio if len(converted_args) > 1: return_buf = converted_args[0] - callsite_args = _callsites[callsite] - stack_args = [] for alloca in callsite_args: if not _is_word_type(alloca.typ): @@ -204,21 +202,20 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio stack_arg = bb.append_instruction("mload", ptr) assert stack_arg is not None stack_args.append(stack_arg) - ret_args.extend(stack_args) if return_buf is not None: if not returns_word: - ret_args.append(return_buf) # type: ignore + stack_args.append(return_buf) if returns_word: - ret_value = bb.append_invoke_instruction(ret_args, returns=True) # type: ignore + ret_value = bb.append_invoke_instruction(stack_args, returns=True) # type: ignore assert ret_value is not None assert isinstance(return_buf, IROperand) # to be optimized out by mem2var bb.append_instruction("mstore", ret_value, return_buf) return return_buf - bb.append_invoke_instruction(ret_args, returns=False) # type: ignore + bb.append_invoke_instruction(stack_args, returns=False) # type: ignore return return_buf From 243625208677ac4f925d19b3aa48524b2c331a10 Mon Sep 17 00:00:00 2001 From: Charles Cooper Date: Sat, 22 Feb 2025 12:35:08 +0100 Subject: [PATCH 58/59] add notes --- vyper/venom/ir_node_to_venom.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index c5912b34e6..9753b64d3d 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -203,6 +203,8 @@ def _handle_self_call(fn: IRFunction, ir: IRnode, symbols: SymbolTable) -> Optio assert stack_arg is not None stack_args.append(stack_arg) + # NOTE: order of return buf vis a vis stack args must be the same + # as the order of params! if return_buf is not None: if not returns_word: stack_args.append(return_buf) @@ -280,6 +282,7 @@ def _handle_internal_func( # (note frontend generates alloca IDs starting from 1) buf = bb.append_instruction("alloca", IRLiteral(-1), IRLiteral(-1), IRLiteral(0)) else: + # NOTE: must match order of stack args! buf = bb.append_instruction("param") bb.instructions[-1].annotation = "return_buffer" From 66a90d08b6ac2a6e415e92457c0d7a094437ee45 Mon Sep 17 00:00:00 2001 From: Harry Kalogirou Date: Mon, 24 Feb 2025 11:11:43 +0200 Subject: [PATCH 59/59] remove `mload` from passthrough --- vyper/venom/ir_node_to_venom.py | 1 - 1 file changed, 1 deletion(-) diff --git a/vyper/venom/ir_node_to_venom.py b/vyper/venom/ir_node_to_venom.py index 9753b64d3d..b5233c647b 100644 --- a/vyper/venom/ir_node_to_venom.py +++ b/vyper/venom/ir_node_to_venom.py @@ -65,7 +65,6 @@ "gasprice", "gaslimit", "returndatasize", - "mload", "iload", "istore", "dload",