Skip to content

Commit f5051e1

Browse files
fix: optimizer regression (#2868)
the optimizer passes introduced in efe1dbe were too aggressive, and would optimize out expressions that had side effects.
1 parent 67bb98a commit f5051e1

File tree

4 files changed

+87
-33
lines changed

4 files changed

+87
-33
lines changed

tests/compiler/ir/test_optimize_ir.py

Lines changed: 34 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
(["eq", 1, 2], [0]),
99
(["lt", 1, 2], [1]),
1010
(["eq", "x", 0], ["iszero", "x"]),
11+
(["eq", ["sload", 0], 0], ["iszero", ["sload", 0]]),
1112
# branch pruner
1213
(["if", ["eq", 1, 2], "pass"], ["seq"]),
1314
(["if", ["eq", 1, 1], 3, 4], [3]),
@@ -22,20 +23,22 @@
2223
(["mstore", 0, ["eq", 1, 2]], ["mstore", 0, 0]),
2324
# conditions
2425
(["ge", "x", 0], [1]), # x >= 0 == True
26+
(["ge", ["sload", 0], 0], None), # no-op
2527
(["iszero", ["gt", "x", 2 ** 256 - 1]], [1]), # x >= MAX_UINT256 == False
2628
(["iszero", ["sgt", "x", 2 ** 255 - 1]], [1]), # signed x >= MAX_INT256 == False
2729
(["le", "x", 0], ["iszero", "x"]),
2830
(["le", 0, "x"], [1]),
31+
(["le", 0, ["sload", 0]], None), # no-op
2932
(["lt", "x", 0], [0]),
3033
(["lt", 0, "x"], ["iszero", ["iszero", "x"]]),
31-
(["gt", 5, "x"], ["lt", "x", 5]),
32-
(["ge", 5, "x"], ["le", "x", 5]),
33-
(["lt", 5, "x"], ["gt", "x", 5]),
34-
(["le", 5, "x"], ["ge", "x", 5]),
35-
(["sgt", 5, "x"], ["slt", "x", 5]),
36-
(["sge", 5, "x"], ["sle", "x", 5]),
37-
(["slt", 5, "x"], ["sgt", "x", 5]),
38-
(["sle", 5, "x"], ["sge", "x", 5]),
34+
(["gt", 5, "x"], None),
35+
(["ge", 5, "x"], None),
36+
(["lt", 5, "x"], None),
37+
(["le", 5, "x"], None),
38+
(["sgt", 5, "x"], None),
39+
(["sge", 5, "x"], None),
40+
(["slt", 5, "x"], None),
41+
(["sle", 5, "x"], None),
3942
(["slt", "x", -(2 ** 255)], ["slt", "x", -(2 ** 255)]), # unimplemented
4043
# tricky conditions
4144
(["sgt", 2 ** 256 - 1, 0], [0]), # -1 > 0
@@ -55,29 +58,37 @@
5558
(["add", 0, "x"], ["x"]),
5659
(["sub", "x", 0], ["x"]),
5760
(["sub", "x", "x"], [0]),
58-
(["sub", ["sload", 0], ["sload", 0]], ["sub", ["sload", 0], ["sload", 0]]), # no-op
59-
(["sub", ["callvalue"], ["callvalue"]], ["sub", ["callvalue"], ["callvalue"]]), # no-op
61+
(["sub", ["sload", 0], ["sload", 0]], None),
62+
(["sub", ["callvalue"], ["callvalue"]], None),
6063
(["mul", "x", 1], ["x"]),
6164
(["div", "x", 1], ["x"]),
6265
(["sdiv", "x", 1], ["x"]),
6366
(["mod", "x", 1], [0]),
67+
(["mod", ["sload", 0], 1], None),
6468
(["smod", "x", 1], [0]),
6569
(["mul", "x", -1], ["sub", 0, "x"]),
6670
(["sdiv", "x", -1], ["sub", 0, "x"]),
6771
(["mul", "x", 0], [0]),
72+
(["mul", ["sload", 0], 0], None),
6873
(["div", "x", 0], [0]),
74+
(["div", ["sload", 0], 0], None),
6975
(["sdiv", "x", 0], [0]),
76+
(["sdiv", ["sload", 0], 0], None),
7077
(["mod", "x", 0], [0]),
78+
(["mod", ["sload", 0], 0], None),
7179
(["smod", "x", 0], [0]),
7280
(["mul", "x", 32], ["shl", 5, "x"]),
7381
(["div", "x", 64], ["shr", 6, "x"]),
7482
(["mod", "x", 128], ["and", "x", 127]),
75-
(["sdiv", "x", 64], ["sdiv", "x", 64]), # no-op
76-
(["smod", "x", 64], ["smod", "x", 64]), # no-op
83+
(["sdiv", "x", 64], None),
84+
(["smod", "x", 64], None),
7785
# bitwise ops
7886
(["shr", 0, "x"], ["x"]),
7987
(["sar", 0, "x"], ["x"]),
8088
(["shl", 0, "x"], ["x"]),
89+
(["shr", 256, "x"], None),
90+
(["sar", 256, "x"], None),
91+
(["shl", 256, "x"], None),
8192
(["and", 1, 2], [0]),
8293
(["or", 1, 2], [3]),
8394
(["xor", 1, 2], [3]),
@@ -87,12 +98,13 @@
8798
(["or", "x", 0], ["x"]),
8899
(["or", 0, "x"], ["x"]),
89100
(["xor", "x", 0], ["x"]),
90-
(["xor", "x", 1], ["xor", "x", 1]), # no-op
91-
(["and", "x", 1], ["and", "x", 1]), # no-op
92-
(["or", "x", 1], ["or", "x", 1]), # no-op
101+
(["xor", "x", 1], None),
102+
(["and", "x", 1], None),
103+
(["or", "x", 1], None),
93104
(["xor", 0, "x"], ["x"]),
94105
(["iszero", ["or", "x", 1]], [0]),
95106
(["iszero", ["or", 2, "x"]], [0]),
107+
(["iszero", ["or", 1, ["sload", 0]]], None),
96108
# nested optimizations
97109
(["eq", 0, ["sub", 1, 1]], [1]),
98110
(["eq", 0, ["add", 2 ** 255, 2 ** 255]], [1]), # test compile-time wrapping
@@ -108,9 +120,13 @@
108120
def test_ir_optimizer(ir):
109121
optimized = optimizer.optimize(IRnode.from_list(ir[0]))
110122
optimized.repr_show_gas = True
111-
hand_optimized = IRnode.from_list(ir[1])
112-
hand_optimized.repr_show_gas = True
113-
assert optimized == hand_optimized
123+
if ir[1] is None:
124+
# no-op, assert optimizer does nothing
125+
expected = IRnode.from_list(ir[0])
126+
else:
127+
expected = IRnode.from_list(ir[1])
128+
expected.repr_show_gas = True
129+
assert optimized == expected
114130

115131

116132
static_assertions_list = [

tests/parser/features/test_internal_call.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,24 @@ def return_hash_of_rzpadded_cow() -> bytes32:
5151
print("Passed single fixed-size argument self-call test")
5252

5353

54+
# test that side-effecting self calls do not get optimized out
55+
def test_selfcall_optimizer(get_contract):
56+
code = """
57+
counter: uint256
58+
59+
@internal
60+
def increment_counter() -> uint256:
61+
self.counter += 1
62+
return self.counter
63+
@external
64+
def foo() -> (uint256, uint256):
65+
x: uint256 = unsafe_mul(self.increment_counter(), 0)
66+
return x, self.counter
67+
"""
68+
c = get_contract(code)
69+
assert c.foo() == [0, 1]
70+
71+
5472
def test_selfcall_code_3(get_contract_with_gas_estimation, keccak):
5573
selfcall_code_3 = """
5674
@internal

vyper/codegen/ir_node.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,7 @@ def gas(self):
268268
def is_complex_ir(self):
269269
# list of items not to cache. note can add other env variables
270270
# which do not change, e.g. calldatasize, coinbase, etc.
271-
do_not_cache = {"~empty"}
271+
do_not_cache = {"~empty", "calldatasize"}
272272
return (
273273
isinstance(self.value, str)
274274
and (self.value.lower() in VALID_IR_MACROS or self.value.upper() in get_ir_opcodes())

vyper/ir/optimizer.py

Lines changed: 34 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,12 @@ def _is_int(node: IRnode) -> bool:
3939
return isinstance(node.value, int)
4040

4141

42+
def _deep_contains(node_or_list, node):
43+
if isinstance(node_or_list, list):
44+
return any(_deep_contains(t, node) for t in node_or_list)
45+
return node is node_or_list
46+
47+
4248
arith = {
4349
"add": (operator.add, "+", UNSIGNED),
4450
"sub": (operator.sub, "-", UNSIGNED),
@@ -109,7 +115,7 @@ def _wrap(x, unsigned=unsigned):
109115
left, right = _int(args[0]), _int(args[1])
110116
new_val = fn(left, right)
111117
new_val = _wrap(new_val)
112-
return False, new_val, [], new_ann
118+
return new_val, [], new_ann
113119

114120
new_val = None
115121
new_args = None
@@ -278,10 +284,16 @@ def _conservative_eq(x, y):
278284
new_val = "iszero"
279285
new_args = [["iszero", args[0]]]
280286

281-
if new_val is None:
282-
return False, binop, args, ann
287+
rollback = (
288+
new_val is None
289+
or (args[0].is_complex_ir and not _deep_contains(new_args, args[0]))
290+
or (args[1].is_complex_ir and not _deep_contains(new_args, args[1]))
291+
)
292+
293+
if rollback:
294+
return None
283295

284-
return True, new_val, new_args, new_ann
296+
return new_val, new_args, new_ann
285297

286298

287299
def optimize(node: IRnode, parent: Optional[IRnode] = None) -> IRnode:
@@ -294,6 +306,16 @@ def optimize(node: IRnode, parent: Optional[IRnode] = None) -> IRnode:
294306
annotation = node.annotation
295307
add_gas_estimate = node.add_gas_estimate
296308

309+
def finalize(ir_builder):
310+
return IRnode.from_list(
311+
ir_builder,
312+
typ=typ,
313+
location=location,
314+
source_pos=source_pos,
315+
annotation=annotation,
316+
add_gas_estimate=add_gas_estimate,
317+
)
318+
297319
optimize_more = False
298320

299321
if value == "seq":
@@ -308,7 +330,11 @@ def optimize(node: IRnode, parent: Optional[IRnode] = None) -> IRnode:
308330

309331
elif value in arith:
310332
parent_op = parent.value if parent is not None else None
311-
optimize_more, value, argz, annotation = _optimize_arith(value, argz, annotation, parent_op)
333+
334+
res = _optimize_arith(value, argz, annotation, parent_op)
335+
if res is not None:
336+
optimize_more = True
337+
value, argz, annotation = res
312338

313339
###
314340
# BITWISE OPS
@@ -369,18 +395,12 @@ def optimize(node: IRnode, parent: Optional[IRnode] = None) -> IRnode:
369395
argz = []
370396

371397
# NOTE: this is really slow (compile-time).
372-
# maybe should optimize the tree in-place
373-
ret = IRnode.from_list(
374-
[value, *argz],
375-
typ=typ,
376-
location=location,
377-
source_pos=source_pos,
378-
annotation=annotation,
379-
add_gas_estimate=add_gas_estimate,
380-
)
398+
# ideal would be to optimize the tree in-place
399+
ret = finalize([value, *argz])
381400

382401
if optimize_more:
383402
ret = optimize(ret, parent=parent)
403+
384404
return ret
385405

386406

0 commit comments

Comments
 (0)