Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
75 changes: 75 additions & 0 deletions tests/unit/compiler/venom/test_analyses_cache.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
from tests.venom_utils import parse_venom
from vyper.venom.analysis import IRAnalysesCache
from vyper.venom.analysis.fcg import FCGGlobalAnalysis
from vyper.venom.basicblock import IRLabel

SRC = """
function entry {
entry:
%1 = invoke @f
stop
}

function f {
f:
%retpc = param
ret %retpc
}
"""


def test_global_cache_creation_registers_self():
ctx = parse_venom(SRC)
entry = ctx.get_function(IRLabel("entry"))

ac = IRAnalysesCache(entry)
ac.request_analysis(FCGGlobalAnalysis)

global_cache = ctx.global_analyses_cache
assert global_cache is not None
# the cache which requested the global analysis must be the registered one
assert global_cache.function_analyses_caches[entry] is ac
# other functions get fresh caches
for fn in ctx.functions.values():
assert fn in global_cache.function_analyses_caches


def test_existing_global_cache_registers_self():
# regression test for https://github.com/vyperlang/vyper/issues/5046
ctx = parse_venom(SRC)
entry = ctx.get_function(IRLabel("entry"))
f = ctx.get_function(IRLabel("f"))

# first request creates the global cache via the `global_cache is None` path
ac_entry = IRAnalysesCache(entry)
ac_entry.request_analysis(FCGGlobalAnalysis)

# second request from a different function's cache takes the fallback path;
# it must register itself in the global cache, not a parallel cache
ac_f = IRAnalysesCache(f)
ac_f.request_analysis(FCGGlobalAnalysis)

global_cache = ctx.global_analyses_cache
assert global_cache is not None
assert global_cache.function_analyses_caches[f] is ac_f


def test_existing_authoritative_cache_not_displaced():
# a real (non-placeholder) cache registered for a function must not be
# displaced by a temporary cache for the same function -- consumers
# rely on the registered cache's invalidations
ctx = parse_venom(SRC)
entry = ctx.get_function(IRLabel("entry"))

ac_entry = IRAnalysesCache(entry)
ac_entry.request_analysis(FCGGlobalAnalysis)

global_cache = ctx.global_analyses_cache
assert global_cache is not None
assert global_cache.function_analyses_caches[entry] is ac_entry

# a second, temporary cache for the same function must defer to the
# registered one
ac_tmp = IRAnalysesCache(entry)
ac_tmp.request_analysis(FCGGlobalAnalysis)
assert global_cache.function_analyses_caches[entry] is ac_entry
27 changes: 22 additions & 5 deletions vyper/venom/analysis/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,22 +67,39 @@ def __init__(self, function: IRFunction):
self.analyses_cache = {}
self.function = function

# marker for caches created as placeholders by
# `_ensure_global_analyses_cache` (as opposed to caches owned by
# a pass pipeline or another "real" consumer, whose invalidations
# downstream consumers rely on)
self._auto_created = False

@classmethod
def _placeholder(cls, function: IRFunction) -> "IRAnalysesCache":
ret = cls(function)
ret._auto_created = True
return ret

def _ensure_global_analyses_cache(self) -> "IRGlobalAnalysesCache":
global_cache = self.function.ctx.global_analyses_cache
if global_cache is None:
function_analyses_caches = {
fn: IRAnalysesCache(fn) for fn in self.function.ctx.functions.values()
fn: IRAnalysesCache._placeholder(fn) for fn in self.function.ctx.functions.values()
}
function_analyses_caches[self.function] = self
global_cache = IRGlobalAnalysesCache(self.function.ctx, function_analyses_caches)
self.function.ctx.global_analyses_cache = global_cache
return global_cache

# register self, but never displace a non-placeholder cache for
# the same function (e.g. one registered by the pass pipeline) --
# its consumers rely on its invalidations.
caches = global_cache.function_analyses_caches
registered = caches.get(self.function)
if registered is None or registered._auto_created:
caches[self.function] = self
for fn in self.function.ctx.functions.values():
if fn not in global_cache.function_analyses_caches:
global_cache.function_analyses_caches[fn] = IRAnalysesCache(fn)
if self.function not in global_cache.function_analyses_caches:
global_cache.function_analyses_caches[self.function] = self
if fn not in caches:
caches[fn] = IRAnalysesCache._placeholder(fn)
return global_cache

def request_analysis(self, analysis_cls: Type[T], *args, **kwargs) -> T:
Expand Down