Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat[venom]: add function inliner #4478

Merged
merged 48 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from 36 commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
28b6591
inliner
harkal Feb 19, 2025
88ddfb0
inliner cloning
harkal Feb 19, 2025
6435f8b
bb copy fix
harkal Feb 19, 2025
bc81fd6
ignore test
harkal Feb 19, 2025
580e667
mark xfail
harkal Feb 19, 2025
9dbbd6c
further cleanup
harkal Feb 19, 2025
42c0c2f
remove extra passes
harkal Feb 19, 2025
60a7929
cleanup callocas
harkal Feb 19, 2025
1469b18
fix source map for insert_instruction
charles-cooper Feb 19, 2025
e0afdc8
cleanup memssa printer
harkal Feb 19, 2025
a8c85fe
Merge branch 'feat/venom/inline_pass' of github.com:harkal/vyper into…
harkal Feb 19, 2025
6de703b
lint
harkal Feb 19, 2025
25d3280
Merge branch 'master' into feat/venom/inline_pass
harkal Feb 19, 2025
0fee1a6
remove dead function
charles-cooper Feb 19, 2025
763e00f
remove dummy line
harkal Feb 20, 2025
b1b3bda
fix expected jumps
charles-cooper Feb 20, 2025
8c3da00
match params by index
charles-cooper Feb 20, 2025
dd9a05c
remove liveness copy
charles-cooper Feb 20, 2025
17d3409
generalize param handling
charles-cooper Feb 20, 2025
64d983f
add notes
charles-cooper Feb 20, 2025
4343ca6
remove magic variables
charles-cooper Feb 20, 2025
817b677
perf: don't create new IRLiteral
charles-cooper Feb 20, 2025
3e97de5
rename to function_inliner
charles-cooper Feb 20, 2025
b3443b0
lint
charles-cooper Feb 20, 2025
7d1721d
style
charles-cooper Feb 20, 2025
12f31da
refactor to use .entry_function
charles-cooper Feb 20, 2025
2d81495
remove redundant loop
charles-cooper Feb 20, 2025
9c29301
fix insert_instruction - only override source info if we are building
charles-cooper Feb 20, 2025
28c5c4f
roll back optimization mode change
charles-cooper Feb 20, 2025
b5e9410
small fixes/comments
charles-cooper Feb 20, 2025
bcf6f05
rename all_nonempty to all2
charles-cooper Feb 20, 2025
08113d7
fix lint
charles-cooper Feb 20, 2025
9fc737a
move a test file
charles-cooper Feb 20, 2025
0ac0533
use tx_failed fixture
charles-cooper Feb 20, 2025
63056bd
remove stray newline
charles-cooper Feb 20, 2025
603e9a0
add notes
charles-cooper Feb 20, 2025
827e729
add review comment
charles-cooper Feb 20, 2025
58a6ed5
add note
charles-cooper Feb 20, 2025
82e144f
add assert back
charles-cooper Feb 20, 2025
96d1023
small fixes
charles-cooper Feb 20, 2025
c217b72
refactor settings threading
charles-cooper Feb 20, 2025
290ff9e
fix lint
charles-cooper Feb 20, 2025
cc192db
generalize label renaming
charles-cooper Feb 20, 2025
a870275
fix lint
charles-cooper Feb 20, 2025
a334de9
Merge remote-tracking branch 'origin-vyper/master' into feat/venom/in…
harkal Feb 20, 2025
34b2023
revert grammar change
charles-cooper Feb 20, 2025
511283f
update inlined variable names, add annotation
charles-cooper Feb 21, 2025
3b15a33
readability improvement for annotations
harkal Feb 21, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
import pytest

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


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, tx_failed):
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 tx_failed():
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)
4 changes: 3 additions & 1 deletion tests/unit/compiler/asm/test_asm_optimizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
16 changes: 12 additions & 4 deletions tests/unit/compiler/test_source_map.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,25 @@ def test_jump_map(optimize, experimental_codegen):
pos_map = source_map["pc_pos_map"]
jump_map = source_map["pc_jump_map"]

expected_jumps = 1
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:
if experimental_codegen:
expected_jumps = 0
expected_internals = 0
else:
expected_jumps = 3
expected_internals = 2
else:
if experimental_codegen:
expected_jumps = 0
expected_internals = 0
else:
expected_jumps = 2
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"]) == 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"]:
Expand Down
32 changes: 32 additions & 0 deletions tests/unit/compiler/venom/test_variable_equivalence.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import itertools

from tests.venom_utils import parse_from_basic_block
from vyper.venom.analysis import DFGAnalysis, IRAnalysesCache
from vyper.venom.basicblock import IRVariable


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 = 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)

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)
5 changes: 5 additions & 0 deletions vyper/codegen/function_definitions/internal_function.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions vyper/codegen/self_call.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,4 +109,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
14 changes: 14 additions & 0 deletions vyper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -693,3 +693,17 @@ def safe_relpath(path):
# on Windows, if path and curdir are on different drives, an exception
# can be thrown
return path


def all2(iterator):
"""
This function checks if all elements in the given `iterable` are truthy,
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.
"""
try:
s = next(iterator)
except StopIteration:
return False
return bool(s) and all(iterator)
24 changes: 19 additions & 5 deletions vyper/venom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
BranchOptimizationPass,
DFTPass,
FloatAllocas,
FunctionInlinerPass,
LoadElimination,
LowerDloadPass,
MakeSSA,
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -94,14 +93,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:
FunctionInlinerPass(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
1 change: 0 additions & 1 deletion vyper/venom/analysis/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions vyper/venom/analysis/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,5 @@ 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)

return self.request_analysis(analysis_cls, *args, **kwargs)
35 changes: 27 additions & 8 deletions vyper/venom/analysis/cfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,29 +34,48 @@
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()

Check warning on line 51 in vyper/venom/analysis/cfg.py

View check run for this annotation

Codecov / codecov/patch

vyper/venom/analysis/cfg.py#L51

Added line #L51 was not covered by tests

def _visit_dfs_pre_r(bb: IRBasicBlock):

Check warning on line 53 in vyper/venom/analysis/cfg.py

View check run for this annotation

Codecov / codecov/patch

vyper/venom/analysis/cfg.py#L53

Added line #L53 was not covered by tests
if bb in visited:
return
visited.add(bb)

Check warning on line 56 in vyper/venom/analysis/cfg.py

View check run for this annotation

Codecov / codecov/patch

vyper/venom/analysis/cfg.py#L55-L56

Added lines #L55 - L56 were not covered by tests

yield bb

Check warning on line 58 in vyper/venom/analysis/cfg.py

View check run for this annotation

Codecov / codecov/patch

vyper/venom/analysis/cfg.py#L58

Added line #L58 was not covered by tests

for out_bb in bb.cfg_out:
yield from _visit_dfs_pre_r(out_bb)

Check warning on line 61 in vyper/venom/analysis/cfg.py

View check run for this annotation

Codecov / codecov/patch

vyper/venom/analysis/cfg.py#L61

Added line #L61 was not covered by tests

yield from _visit_dfs_pre_r(self.function.entry)

Check warning on line 63 in vyper/venom/analysis/cfg.py

View check run for this annotation

Codecov / codecov/patch

vyper/venom/analysis/cfg.py#L63

Added line #L63 was not covered by tests

@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
19 changes: 18 additions & 1 deletion vyper/venom/analysis/dfg.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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
Expand Down
22 changes: 11 additions & 11 deletions vyper/venom/analysis/dominators.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -80,26 +80,26 @@ 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)

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
Expand Down Expand Up @@ -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:
"""
Expand Down
Loading
Loading