Skip to content

Commit 0b93624

Browse files
authored
fix[venom]: fix sccp TOP evaluation order (#4748)
This commit updates SCCP to not assert during arithmetic evaluation if any operand’s lattice value was not a literal, and instead `SCCP._eval` returns TOP when any operand is TOP (defers constant folding) and keep existing semantics for BOTTOM and literals. Adds a test that triggers the assertion without the fix. Reschedule instructions in generic case when setting to BOTTOM.
1 parent 2e57901 commit 0b93624

File tree

2 files changed

+107
-13
lines changed

2 files changed

+107
-13
lines changed

tests/unit/compiler/venom/test_sccp.py

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
from tests.venom_utils import PrePostChecker
44
from vyper.exceptions import StaticAssertionException
5+
from vyper.venom.analysis.analysis import IRAnalysesCache
56
from vyper.venom.basicblock import IRVariable
7+
from vyper.venom.parser import parse_venom
68
from vyper.venom.passes import SCCP
79
from vyper.venom.passes.sccp.sccp import LatticeEnum
810

@@ -278,6 +280,75 @@ def test_cont_phi_const_case():
278280
assert sccp.lattice[IRVariable("%5:2")] == LatticeEnum.TOP
279281

280282

283+
def test_sccp_phi_operand_top_no_branch():
284+
"""
285+
control jumps directly to a join block where a phi depends on predecessors
286+
that haven't been executed yet. The phi is TOP at first, and hhe arithmetic
287+
using it must defer evaluation.
288+
"""
289+
# NOTE: `main` goes straight to `@join`, yet the phi still lists `@then`
290+
# and `@else` as inputs. This intentionally mimics malformed IR seen in
291+
# programs where the CFG includes those predecessors even though
292+
# execution never reaches them (and will be prunned by a later pass).
293+
# So here we show that can SCCP gracefully treat the phi inputs
294+
# as TOP until (and unless) those blocks are actually visited. Decoupling
295+
# essentially the CGF from the SCCP.
296+
pre = """
297+
main:
298+
jmp @join
299+
then:
300+
%a_then = 2
301+
jmp @join
302+
else:
303+
%a_else = 3
304+
jmp @join
305+
join:
306+
%phi = phi @then, %a_then, @else, %a_else
307+
%out = sub 14, %phi
308+
sink %out
309+
"""
310+
311+
_check_pre_post(pre, pre, hevm=False)
312+
313+
314+
def test_sccp_jnz_top_phi_text_ir():
315+
"""
316+
Same as above but using the value to control a jnz.
317+
This used to assert in SCCP when the jnz condition was TOP.
318+
"""
319+
# NOTE: `main` goes straight to `@join`, yet the phi still lists `@then`
320+
# and `@else` as inputs. This intentionally mimics malformed IR seen in
321+
# programs where the CFG includes those predecessors even though
322+
# execution never reaches them (and will be prunned by a later pass).
323+
# So here we show that can SCCP gracefully treat the phi inputs
324+
# as TOP until (and unless) those blocks are actually visited. Decoupling
325+
# essentially the CGF from the SCCP.
326+
src = """
327+
function main {
328+
main:
329+
jmp @join
330+
then:
331+
%a_then = 2
332+
jmp @join
333+
else:
334+
%a_else = 3
335+
jmp @join
336+
join:
337+
%phi = phi @then, %a_then, @else, %a_else
338+
jnz %phi, @true, @false
339+
true:
340+
sink 1
341+
false:
342+
sink 2
343+
}
344+
"""
345+
346+
ctx = parse_venom(src)
347+
fn = ctx.get_function(next(iter(ctx.functions.keys())))
348+
ac = IRAnalysesCache(fn)
349+
SCCP(ac, fn).run_pass()
350+
351+
281352
def test_phi_reduction_without_basic_block_removal():
282353
"""
283354
Test of phi reduction `if` end not `if-else`
@@ -307,3 +378,30 @@ def test_phi_reduction_without_basic_block_removal():
307378
"""
308379

309380
_check_pre_post(pre, post)
381+
382+
383+
inst = ["mload", "sload", "dload", "iload", "calldataload", "param"]
384+
385+
386+
@pytest.mark.parametrize("inst", inst)
387+
def test_mload_schedules_uses(inst):
388+
pre = f"""
389+
main:
390+
%cond = param
391+
jnz %cond, @B, @A
392+
A:
393+
%m = {inst} 0
394+
jmp @join
395+
B:
396+
%x = assign %m
397+
jmp @join
398+
join:
399+
sink %x
400+
"""
401+
402+
passes = _check_pre_post(pre, pre, hevm=False)
403+
sccp = passes[0]
404+
assert isinstance(sccp, SCCP)
405+
406+
assert sccp.lattice[IRVariable("%m")] == LatticeEnum.BOTTOM
407+
assert sccp.lattice[IRVariable("%x")] == LatticeEnum.BOTTOM

vyper/venom/passes/sccp/sccp.py

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,7 @@ def _visit_expr(self, inst: IRInstruction):
195195
elif opcode == "jnz":
196196
lat = self._eval_from_lattice(inst.operands[0])
197197

198-
assert lat != LatticeEnum.TOP, f"Got undefined var at jmp at {inst.parent}"
199-
if lat == LatticeEnum.BOTTOM:
198+
if lat in (LatticeEnum.BOTTOM, LatticeEnum.TOP):
200199
for out_bb in self.cfg.cfg_out(inst.parent):
201200
self.work_list.append(FlowWorkItem(inst.parent, out_bb))
202201
else:
@@ -210,24 +209,18 @@ def _visit_expr(self, inst: IRInstruction):
210209
self.work_list.append(FlowWorkItem(inst.parent, target))
211210
elif opcode == "djmp":
212211
lat = self._eval_from_lattice(inst.operands[0])
213-
assert lat != LatticeEnum.TOP, f"Got undefined var at jmp at {inst.parent}"
214-
if lat == LatticeEnum.BOTTOM:
212+
if lat in (LatticeEnum.BOTTOM, LatticeEnum.TOP):
215213
for op in inst.operands[1:]:
216214
target = self.fn.get_basic_block(op.name)
217215
self.work_list.append(FlowWorkItem(inst.parent, target))
218216
elif isinstance(lat, IRLiteral):
219217
raise CompilerPanic("Unimplemented djmp with literal")
220-
221-
elif opcode in ["param", "calldataload"]:
222-
self.lattice[inst.output] = LatticeEnum.BOTTOM # type: ignore
223-
self._add_ssa_work_items(inst)
224-
elif opcode == "mload":
225-
self.lattice[inst.output] = LatticeEnum.BOTTOM # type: ignore
226218
elif opcode in ARITHMETIC_OPS:
227219
self._eval(inst)
228220
else:
229221
if inst.output is not None:
230222
self._set_lattice(inst.output, LatticeEnum.BOTTOM)
223+
self._add_ssa_work_items(inst)
231224

232225
def _eval(self, inst) -> LatticeItem:
233226
"""
@@ -257,12 +250,15 @@ def finalize(ret):
257250
assert isinstance(op, IRLiteral) # clarity
258251
eval_result = op
259252

260-
# The value from the lattice should have evaluated to BOTTOM
261-
# or a literal by now.
253+
# The value from the lattice should have evaluated to TOP,
254+
# BOTTOM, or a literal by now.
255+
262256
# If any operand is BOTTOM, the whole operation is BOTTOM
263-
# and we can stop the evaluation early
264257
if eval_result is LatticeEnum.BOTTOM:
265258
return finalize(LatticeEnum.BOTTOM)
259+
# If any operand is TOP the operation is TOP
260+
if eval_result is LatticeEnum.TOP:
261+
return finalize(LatticeEnum.TOP)
266262

267263
assert isinstance(eval_result, IRLiteral), (inst.parent.label, op, inst)
268264
ops.append(eval_result)

0 commit comments

Comments
 (0)