Skip to content

Commit 7c90ace

Browse files
duncancmtclaude
andcommitted
fix: add constant propagation to _inline_yul_function
_inline_yul_function was the only sequential statement processor that did not maintain a substitution map. When an intermediate variable was assigned a constant (e.g. `let expr_1 := cleanup(3)`) and then passed to a helper with branch expr_stmts (e.g. `wrapping_div(x, expr_1)`), the helper saw `Var('expr_1')` instead of `IntLit(3)`, making the branch condition non-constant and incorrectly rejecting the code. Add a `const_subst` dict that tracks variables assigned to compile-time-constant values: - Before inline_calls: substitute known constants into expressions - After inline_calls on PlainAssignment: record constant results, or remove the variable if the new value is non-constant - After ParsedIfBlock: invalidate conditionally-assigned variables Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent b9ad46d commit 7c90ace

File tree

2 files changed

+184
-17
lines changed

2 files changed

+184
-17
lines changed

formal/test_yul_to_lean.py

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7620,6 +7620,168 @@ def test_constant_switch_dead_branch_expr_stmt_discarded_in_target(self) -> None
76207620
model = result.models[0]
76217621
self.assertEqual(ytl.evaluate_function_model(model, ()), (7,))
76227622

7623+
# -- Constant propagation across statements in _inline_yul_function --
7624+
7625+
def test_wrapping_div_pattern_intermediate_var(self) -> None:
7626+
"""Constant divisor via intermediate variable is resolved during inlining."""
7627+
yul = """
7628+
function fun_target_1(var_x_1) -> var_z_2 {
7629+
let expr_1 := cleanup(3)
7630+
var_z_2 := wrapping_div(var_x_1, expr_1)
7631+
}
7632+
function wrapping_div(x, y) -> r {
7633+
if iszero(y) { panic_error_0x12() }
7634+
r := div(x, y)
7635+
}
7636+
function cleanup(value) -> cleaned {
7637+
cleaned := value
7638+
}
7639+
"""
7640+
config = make_model_config(("target",))
7641+
result = ytl.translate_yul_to_models(
7642+
yul,
7643+
config,
7644+
pipeline=ytl.RAW_TRANSLATION_PIPELINE,
7645+
)
7646+
model = result.models[0]
7647+
self.assertEqual(ytl.evaluate_function_model(model, (9,)), (3,))
7648+
7649+
def test_wrapping_div_pattern_non_constant_var_rejected(self) -> None:
7650+
"""Non-constant variable arg: branch with expr_stmt is correctly rejected."""
7651+
yul = """
7652+
function fun_target_1(var_x_1, var_y_2) -> var_z_3 {
7653+
let expr_1 := cleanup(var_y_2)
7654+
var_z_3 := wrapping_div(var_x_1, expr_1)
7655+
}
7656+
function wrapping_div(x, y) -> r {
7657+
if iszero(y) { panic_error_0x12() }
7658+
r := div(x, y)
7659+
}
7660+
function cleanup(value) -> cleaned {
7661+
cleaned := value
7662+
}
7663+
"""
7664+
config = make_model_config(("target",))
7665+
with self.assertRaises(ytl.ParseError):
7666+
ytl.translate_yul_to_models(
7667+
yul,
7668+
config,
7669+
pipeline=ytl.RAW_TRANSLATION_PIPELINE,
7670+
)
7671+
7672+
def test_wrapping_div_pattern_dead_branch_after_cleanup_inline(self) -> None:
7673+
"""Real wrapping_div_t_uint256 pattern: cleanup identity inlined before fold."""
7674+
yul = """
7675+
function fun_target_1(var_x_1) -> var_z_2 {
7676+
var_z_2 := wrapping_div_t_uint256(var_x_1, 3)
7677+
}
7678+
function wrapping_div_t_uint256(x, y) -> r {
7679+
x := cleanup_t_uint256(x)
7680+
y := cleanup_t_uint256(y)
7681+
if iszero(y) { panic_error_0x12() }
7682+
r := div(x, y)
7683+
}
7684+
function cleanup_t_uint256(value) -> cleaned {
7685+
cleaned := value
7686+
}
7687+
"""
7688+
config = make_model_config(("target",))
7689+
result = ytl.translate_yul_to_models(
7690+
yul,
7691+
config,
7692+
pipeline=ytl.RAW_TRANSLATION_PIPELINE,
7693+
)
7694+
model = result.models[0]
7695+
self.assertEqual(ytl.evaluate_function_model(model, (9,)), (3,))
7696+
7697+
# -- const_subst invalidation edge cases --
7698+
7699+
def test_const_subst_invalidated_after_non_constant_reassignment(self) -> None:
7700+
"""Variable reassigned to non-constant must be removed from const_subst.
7701+
7702+
Without invalidation, ``usr$x`` would stay as ``IntLit(3)`` and the
7703+
call would see ``wrapping_div(3, 3)`` instead of
7704+
``wrapping_div(add(3, y), 3)``.
7705+
"""
7706+
yul = """
7707+
function fun_target_1(var_y_1) -> var_z_2 {
7708+
let usr$x := 3
7709+
usr$x := add(usr$x, var_y_1)
7710+
var_z_2 := wrapping_div(usr$x, 3)
7711+
}
7712+
function wrapping_div(x, y) -> r {
7713+
if iszero(y) { panic_error_0x12() }
7714+
r := div(x, y)
7715+
}
7716+
"""
7717+
config = make_model_config(("target",))
7718+
result = ytl.translate_yul_to_models(
7719+
yul,
7720+
config,
7721+
pipeline=ytl.RAW_TRANSLATION_PIPELINE,
7722+
)
7723+
model = result.models[0]
7724+
# (3 + 6) / 3 == 3
7725+
self.assertEqual(ytl.evaluate_function_model(model, (6,)), (3,))
7726+
# (3 + 0) / 3 == 1
7727+
self.assertEqual(ytl.evaluate_function_model(model, (0,)), (1,))
7728+
7729+
def test_const_subst_from_constant_true_if_body(self) -> None:
7730+
"""Variable assigned inside ``if 1 { x := 3 }`` should be usable as constant.
7731+
7732+
The condition is statically true, so ``x := 3`` always executes.
7733+
The constant should propagate to subsequent statements.
7734+
"""
7735+
yul = """
7736+
function fun_target_1(var_a_1) -> var_z_2 {
7737+
let usr$x := 0
7738+
if 1 {
7739+
usr$x := 3
7740+
}
7741+
var_z_2 := wrapping_div(var_a_1, usr$x)
7742+
}
7743+
function wrapping_div(x, y) -> r {
7744+
if iszero(y) { panic_error_0x12() }
7745+
r := div(x, y)
7746+
}
7747+
"""
7748+
config = make_model_config(("target",))
7749+
result = ytl.translate_yul_to_models(
7750+
yul,
7751+
config,
7752+
pipeline=ytl.RAW_TRANSLATION_PIPELINE,
7753+
)
7754+
model = result.models[0]
7755+
self.assertEqual(ytl.evaluate_function_model(model, (9,)), (3,))
7756+
7757+
def test_const_subst_not_poisoned_by_block_scoped_shadow(self) -> None:
7758+
"""Block-scoped ``let x := 5`` inside ``if 1`` must not overwrite outer ``x``.
7759+
7760+
After ``if 1 { let x := 5 }`` the outer ``x`` is still ``3``.
7761+
"""
7762+
yul = """
7763+
function fun_target_1(var_a_1) -> var_z_2 {
7764+
let usr$x := 3
7765+
if 1 {
7766+
let usr$x := 5
7767+
}
7768+
var_z_2 := wrapping_div(var_a_1, usr$x)
7769+
}
7770+
function wrapping_div(x, y) -> r {
7771+
if iszero(y) { panic_error_0x12() }
7772+
r := div(x, y)
7773+
}
7774+
"""
7775+
config = make_model_config(("target",))
7776+
result = ytl.translate_yul_to_models(
7777+
yul,
7778+
config,
7779+
pipeline=ytl.RAW_TRANSLATION_PIPELINE,
7780+
)
7781+
model = result.models[0]
7782+
# outer x is 3, so 9 / 3 == 3 (not 9 / 5)
7783+
self.assertEqual(ytl.evaluate_function_model(model, (9,)), (3,))
7784+
76237785

76247786
if __name__ == "__main__":
76257787
unittest.main()

formal/yul_to_lean.py

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3099,6 +3099,7 @@ def _inline_yul_function(
30993099

31003100
mstore_sink: list[FromWriteEffect] = []
31013101
new_assignments: list[RawStatement] = []
3102+
const_subst: dict[str, Expr] = {}
31023103
for stmt in yf.assignments:
31033104
if isinstance(stmt, ParsedIfBlock):
31043105
_reject_branch_expr_stmts(
@@ -3107,7 +3108,7 @@ def _inline_yul_function(
31073108
)
31083109
pre_len = len(mstore_sink)
31093110
new_cond = inline_calls(
3110-
stmt.condition,
3111+
substitute_expr(stmt.condition, const_subst),
31113112
fn_table,
31123113
mstore_sink=mstore_sink,
31133114
unsupported_function_errors=unsupported_function_errors,
@@ -3118,7 +3119,7 @@ def _inline_yul_function(
31183119
PlainAssignment(
31193120
s.target,
31203121
inline_calls(
3121-
s.expr,
3122+
substitute_expr(s.expr, const_subst),
31223123
fn_table,
31233124
mstore_sink=mstore_sink,
31243125
unsupported_function_errors=unsupported_function_errors,
@@ -3134,7 +3135,7 @@ def _inline_yul_function(
31343135
PlainAssignment(
31353136
s.target,
31363137
inline_calls(
3137-
s.expr,
3138+
substitute_expr(s.expr, const_subst),
31383139
fn_table,
31393140
mstore_sink=mstore_sink,
31403141
unsupported_function_errors=unsupported_function_errors,
@@ -3148,28 +3149,28 @@ def _inline_yul_function(
31483149
"inlining a control-flow block. Exact uint512.from(...) "
31493150
"accessor writes must stay on the straight-line path."
31503151
)
3151-
new_assignments.append(
3152-
ParsedIfBlock(
3153-
condition=new_cond,
3154-
body=tuple(new_body),
3155-
has_leave=stmt.has_leave,
3156-
else_body=(
3157-
tuple(new_else_body) if new_else_body is not None else None
3158-
),
3159-
body_expr_stmts=stmt.body_expr_stmts,
3160-
else_body_expr_stmts=stmt.else_body_expr_stmts,
3161-
)
3152+
new_if = ParsedIfBlock(
3153+
condition=new_cond,
3154+
body=tuple(new_body),
3155+
has_leave=stmt.has_leave,
3156+
else_body=(tuple(new_else_body) if new_else_body is not None else None),
3157+
body_expr_stmts=stmt.body_expr_stmts,
3158+
else_body_expr_stmts=stmt.else_body_expr_stmts,
31623159
)
3160+
# Invalidate any variables conditionally assigned
3161+
for t in _stmt_targets(new_if):
3162+
const_subst.pop(t, None)
3163+
new_assignments.append(new_if)
31633164
elif isinstance(stmt, MemoryWrite):
31643165
pre_len = len(mstore_sink)
31653166
new_addr = inline_calls(
3166-
stmt.address,
3167+
substitute_expr(stmt.address, const_subst),
31673168
fn_table,
31683169
mstore_sink=mstore_sink,
31693170
unsupported_function_errors=unsupported_function_errors,
31703171
)
31713172
new_value = inline_calls(
3172-
stmt.value,
3173+
substitute_expr(stmt.value, const_subst),
31733174
fn_table,
31743175
mstore_sink=mstore_sink,
31753176
unsupported_function_errors=unsupported_function_errors,
@@ -3184,11 +3185,15 @@ def _inline_yul_function(
31843185
else:
31853186
pre_len = len(mstore_sink)
31863187
inlined = inline_calls(
3187-
stmt.expr,
3188+
substitute_expr(stmt.expr, const_subst),
31883189
fn_table,
31893190
mstore_sink=mstore_sink,
31903191
unsupported_function_errors=unsupported_function_errors,
31913192
)
3193+
if _try_const_eval(inlined) is not None:
3194+
const_subst[stmt.target] = inlined
3195+
else:
3196+
const_subst.pop(stmt.target, None)
31923197
for effect in mstore_sink[pre_len:]:
31933198
new_assignments.extend(effect.lower())
31943199
del mstore_sink[pre_len:]

0 commit comments

Comments
 (0)