-
-
Notifications
You must be signed in to change notification settings - Fork 3k
Constant fold initializers of final variables #14283
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
Changes from all commits
7a263af
7d3ed6d
92f25b9
b0efc08
ea4427d
44c4f57
ad26e3d
42eed84
9827c18
11e0d97
4fa9fd3
815cf94
1a1320e
089f3c4
e367d5f
5d5c4fe
8856ff7
2ee1904
d6e7156
bb649ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
"""Constant folding of expressions. | ||
|
||
For example, 3 + 5 can be constant folded into 8. | ||
""" | ||
|
||
from __future__ import annotations | ||
|
||
from typing import Union | ||
from typing_extensions import Final | ||
|
||
from mypy.nodes import Expression, FloatExpr, IntExpr, NameExpr, OpExpr, StrExpr, UnaryExpr, Var | ||
|
||
# All possible result types of constant folding | ||
ConstantValue = Union[int, bool, float, str] | ||
CONST_TYPES: Final = (int, bool, float, str) | ||
|
||
|
||
def constant_fold_expr(expr: Expression, cur_mod_id: str) -> ConstantValue | None: | ||
"""Return the constant value of an expression for supported operations. | ||
|
||
Among other things, support int arithmetic and string | ||
concatenation. For example, the expression 3 + 5 has the constant | ||
value 8. | ||
|
||
Also bind simple references to final constants defined in the | ||
current module (cur_mod_id). Binding to references is best effort | ||
-- we don't bind references to other modules. Mypyc trusts these | ||
to be correct in compiled modules, so that it can replace a | ||
constant expression (or a reference to one) with the statically | ||
computed value. We don't want to infer constant values based on | ||
stubs, in particular, as these might not match the implementation | ||
(due to version skew, for example). | ||
|
||
Return None if unsuccessful. | ||
""" | ||
if isinstance(expr, IntExpr): | ||
return expr.value | ||
if isinstance(expr, StrExpr): | ||
return expr.value | ||
if isinstance(expr, FloatExpr): | ||
return expr.value | ||
elif isinstance(expr, NameExpr): | ||
if expr.name == "True": | ||
return True | ||
elif expr.name == "False": | ||
return False | ||
node = expr.node | ||
if ( | ||
isinstance(node, Var) | ||
and node.is_final | ||
and node.fullname.rsplit(".", 1)[0] == cur_mod_id | ||
): | ||
value = node.final_value | ||
if isinstance(value, (CONST_TYPES)): | ||
return value | ||
elif isinstance(expr, OpExpr): | ||
left = constant_fold_expr(expr.left, cur_mod_id) | ||
right = constant_fold_expr(expr.right, cur_mod_id) | ||
if isinstance(left, int) and isinstance(right, int): | ||
return constant_fold_binary_int_op(expr.op, left, right) | ||
elif isinstance(left, str) and isinstance(right, str): | ||
return constant_fold_binary_str_op(expr.op, left, right) | ||
elif isinstance(expr, UnaryExpr): | ||
value = constant_fold_expr(expr.expr, cur_mod_id) | ||
if isinstance(value, int): | ||
return constant_fold_unary_int_op(expr.op, value) | ||
return None | ||
|
||
|
||
def constant_fold_binary_int_op(op: str, left: int, right: int) -> int | None: | ||
if op == "+": | ||
return left + right | ||
if op == "-": | ||
return left - right | ||
elif op == "*": | ||
return left * right | ||
elif op == "//": | ||
if right != 0: | ||
return left // right | ||
elif op == "%": | ||
if right != 0: | ||
return left % right | ||
elif op == "&": | ||
return left & right | ||
elif op == "|": | ||
return left | right | ||
elif op == "^": | ||
return left ^ right | ||
elif op == "<<": | ||
if right >= 0: | ||
return left << right | ||
elif op == ">>": | ||
if right >= 0: | ||
return left >> right | ||
elif op == "**": | ||
if right >= 0: | ||
ret = left**right | ||
assert isinstance(ret, int) | ||
return ret | ||
return None | ||
|
||
|
||
def constant_fold_unary_int_op(op: str, value: int) -> int | None: | ||
if op == "-": | ||
return -value | ||
elif op == "~": | ||
return ~value | ||
elif op == "+": | ||
return value | ||
return None | ||
|
||
|
||
def constant_fold_binary_str_op(op: str, left: str, right: str) -> str | None: | ||
if op == "+": | ||
return left + right | ||
return None |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3273,7 +3273,7 @@ L2: | |
[case testFinalStaticInt] | ||
from typing import Final | ||
|
||
x: Final = 1 + 1 | ||
x: Final = 1 + int() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The intent of changes like these is to preserve the old test case behavior where constant folding wasn't performed. |
||
|
||
def f() -> int: | ||
return x - 1 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -205,23 +205,13 @@ Y: Final = 2 + 4 | |
|
||
def f() -> None: | ||
a = X + 1 | ||
# TODO: Constant fold this as well | ||
a = Y + 1 | ||
[out] | ||
def f(): | ||
a, r0 :: int | ||
r1 :: bool | ||
r2 :: int | ||
a :: int | ||
L0: | ||
a = 12 | ||
r0 = __main__.Y :: static | ||
if is_error(r0) goto L1 else goto L2 | ||
L1: | ||
r1 = raise NameError('value for final name "Y" was not set') | ||
unreachable | ||
L2: | ||
r2 = CPyTagged_Add(r0, 2) | ||
a = r2 | ||
a = 14 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This illustrates how much we can simplify the generated code. The simpler code is also significantly faster, since there is no memory read any more. |
||
return 1 | ||
|
||
[case testIntConstantFoldingClassFinal] | ||
|
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Interesting, this is very similar logic to the code @JelleZijlstra added in
evalexpr.py
in a9c62c5. I wonder if that logic could be reused here?(Apologies if this comment makes no sense; I don't know much about mypyc internals!)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, there is definitely some overlap. Right now the benefits of sharing code don't seem big enough to me to make it worthwhile to increase coupling, but if we add support for more things, it may make sense to refactor and share parts of the implementations. Note that mypy and mypyc already share some of the constant folding code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a visitor?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not using a visitor, since only relatively few AST node types are handled here, and others have shared default behavior. I generally use a visitor when I need to implement logic for many node types. However, I think that in this case both using and not using a visitor would have been reasonable.