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 22 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
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
64 changes: 64 additions & 0 deletions tests/unit/compiler/venom/test_inliner.py
Original file line number Diff line number Diff line change
@@ -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)
38 changes: 38 additions & 0 deletions tests/unit/compiler/venom/test_variable_equivalence.py
Original file line number Diff line number Diff line change
@@ -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)
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
2 changes: 1 addition & 1 deletion vyper/compiler/phases.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions vyper/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
26 changes: 21 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,
FuncInlinerPass,
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 @@ -84,6 +83,8 @@ 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()

StoreExpansionPass(ac, fn).run_pass()
Expand All @@ -94,14 +95,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():
ir_analyses[fn] = IRAnalysesCache(fn)

_run_global_passes(ctx, optimize, ir_analyses)

ir_analyses = {}
for fn in ctx.functions.values():
_run_passes(fn, optimize)
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
4 changes: 4 additions & 0 deletions vyper/venom/analysis/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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 @@ 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
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
Loading
Loading