Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
970cd4f
[mypyc] feat: add IndexOp support to `constant_fold_expr`
BobTheBuidler Oct 2, 2025
51fdd2d
Update constant_fold.py
BobTheBuidler Oct 2, 2025
4ad507a
Update constant_fold.py
BobTheBuidler Oct 2, 2025
bed37d6
Update constant_fold.py
BobTheBuidler Oct 2, 2025
d7f75f4
Update constant_fold.py
BobTheBuidler Oct 2, 2025
558dee1
Update constant_fold.py
BobTheBuidler Oct 2, 2025
3f18ed0
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 2, 2025
692844c
Update constant_fold.py
BobTheBuidler Oct 2, 2025
869616c
Update constant_fold.py
BobTheBuidler Oct 2, 2025
22651fe
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 2, 2025
9bd78f3
Update constant_fold.py
BobTheBuidler Oct 2, 2025
60574fc
Update constant_fold.py
BobTheBuidler Oct 2, 2025
daeaa50
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 2, 2025
785c221
Update constant_fold.py
BobTheBuidler Oct 2, 2025
817ab5b
Merge branch 'master' into patch-12
BobTheBuidler Oct 2, 2025
042f726
Update irbuild-constant-fold.test
BobTheBuidler Oct 2, 2025
fe2f670
Update irbuild-constant-fold.test
BobTheBuidler Oct 2, 2025
377b31e
refactor
BobTheBuidler Oct 2, 2025
adae39f
Update expression.py
BobTheBuidler Oct 2, 2025
35b0dfc
Update constant_fold.py
BobTheBuidler Oct 2, 2025
6a25595
Update constant_fold.py
BobTheBuidler Oct 2, 2025
8add115
Update constant_fold.py
BobTheBuidler Oct 2, 2025
286a108
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Oct 2, 2025
0a14359
Update constant_fold.py
BobTheBuidler Oct 2, 2025
2d768df
Update constant_fold.py
BobTheBuidler Oct 2, 2025
4bcd5d6
Update constant_fold.py
BobTheBuidler Oct 2, 2025
a62ef3a
Update constant_fold.py
BobTheBuidler Oct 2, 2025
3ff12ec
Update constant_fold.py
BobTheBuidler Oct 2, 2025
45bcc66
Update irbuild-constant-fold.test
BobTheBuidler Oct 2, 2025
e2fa664
Update run-strings.test
BobTheBuidler Oct 2, 2025
27193d8
Update irbuild-constant-fold.test
BobTheBuidler Oct 2, 2025
4dc3a78
Merge branch 'master' into patch-12
BobTheBuidler Oct 4, 2025
9481314
Merge branch 'master' into patch-12
BobTheBuidler Oct 6, 2025
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
36 changes: 36 additions & 0 deletions mypy/constant_fold.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@
ComplexExpr,
Expression,
FloatExpr,
IndexExpr,
IntExpr,
NameExpr,
OpExpr,
SliceExpr,
StrExpr,
UnaryExpr,
Var,
Expand Down Expand Up @@ -73,6 +75,40 @@ def constant_fold_expr(expr: Expression, cur_mod_id: str) -> ConstantValue | Non
value = constant_fold_expr(expr.expr, cur_mod_id)
if value is not None:
return constant_fold_unary_op(expr.op, value)
elif isinstance(expr, IndexExpr):
base = constant_fold_expr(expr.base, cur_mod_id)
if base is not None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be better to check if base is a Sequence -- you can index an int, for example.

index_expr = expr.index
if isinstance(index_expr, SliceExpr):
if index_expr.begin_index is None:
begin_index = None
else:
begin_index = constant_fold_expr(index_expr.begin_index, cur_mod_id)
if begin_index is None:
return None
if index_expr.end_index is None:
end_index = None
else:
end_index = constant_fold_expr(index_expr.end_index, cur_mod_id)
if end_index is None:
return None
if index_expr.stride is None:
stride = None
else:
stride = constant_fold_expr(index_expr.stride, cur_mod_id)
if stride is None:
return None
try:
return base[begin_index:end_index:stride] # type: ignore [index, misc]
except Exception:
return None

index = constant_fold_expr(index_expr, cur_mod_id)
if index is not None:
try:
return base[index] # type: ignore [index]
except Exception:
return None
return None


Expand Down
69 changes: 68 additions & 1 deletion mypyc/irbuild/constant_fold.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,22 +10,25 @@

from __future__ import annotations

from typing import TYPE_CHECKING, Final, Union
from typing import TYPE_CHECKING, Callable, Final, TypeVar, Union

from mypy.constant_fold import constant_fold_binary_op, constant_fold_unary_op
from mypy.nodes import (
BytesExpr,
ComplexExpr,
Expression,
FloatExpr,
IndexExpr,
IntExpr,
MemberExpr,
NameExpr,
OpExpr,
SliceExpr,
StrExpr,
UnaryExpr,
Var,
)
from mypyc.ir.ops import Value
from mypyc.irbuild.util import bytes_from_str

if TYPE_CHECKING:
Expand All @@ -35,6 +38,8 @@
ConstantValue = Union[int, float, complex, str, bytes]
CONST_TYPES: Final = (int, float, complex, str, bytes)

Expr = TypeVar("Expr", bound=Expression)


def constant_fold_expr(builder: IRBuilder, expr: Expression) -> ConstantValue | None:
"""Return the constant value of an expression for supported operations.
Expand Down Expand Up @@ -74,6 +79,40 @@ def constant_fold_expr(builder: IRBuilder, expr: Expression) -> ConstantValue |
value = constant_fold_expr(builder, expr.expr)
if value is not None and not isinstance(value, bytes):
return constant_fold_unary_op(expr.op, value)
elif isinstance(expr, IndexExpr):
base = constant_fold_expr(builder, expr.base)
if base is not None:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar -- check if base is a Sequence?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I left out the type check to make it maximally receptive to future constant folding changes

For example, while we can't constant fold a dict value as a real constant, we can constant fold it as an intermediate step of an existing constant fold

This isn't relevant at the moment, as Sequence would catch all existing cases, but it will be relevant in the coming weeks as I keep enhancing our folding capabilities

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In either case the program is still safe to run in its current state, but only with the None check do we get future extensibility built in

index_expr = expr.index
if isinstance(index_expr, SliceExpr):
if index_expr.begin_index is None:
begin_index = None
else:
begin_index = constant_fold_expr(builder, index_expr.begin_index)
if begin_index is None:
return None
if index_expr.end_index is None:
end_index = None
else:
end_index = constant_fold_expr(builder, index_expr.end_index)
if end_index is None:
return None
if index_expr.stride is None:
stride = None
else:
stride = constant_fold_expr(builder, index_expr.stride)
if stride is None:
return None
try:
return base[begin_index:end_index:stride] # type: ignore [index, misc]
except Exception:
return None

index = constant_fold_expr(builder, index_expr)
if index is not None:
try:
return base[index] # type: ignore [index]
except Exception:
return None
return None


Expand All @@ -95,3 +134,31 @@ def constant_fold_binary_op_extended(
return left * right

return None


def try_constant_fold(builder: IRBuilder, expr: Expression) -> Value | None:
"""Return the constant value of an expression if possible.

Return None otherwise.
"""
value = constant_fold_expr(builder, expr)
if value is not None:
return builder.load_literal_value(value)
return None


def folding_candidate(
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a new decorator that lets us deduplicate existing folding code and also sets me up for cleaner implementation of folding transformations of cast and comparison when those PRs are ready

I think you'll likely agree that both of these functions are better suited here in the constant folding file

transform: Callable[[IRBuilder, Expr], Value],
) -> Callable[[IRBuilder, Expr], Value]:
"""Mark a transform function as a candidate for constant folding.

Candidate functions will attempt to short-circuit the transformation
by constant folding the expression and will only proceed to transform
the expression if folding is not possible.
"""

def constant_fold_wrap(builder: IRBuilder, expr: Expr) -> Value:
folded = try_constant_fold(builder, expr)
return folded if folded is not None else transform(builder, expr)

return constant_fold_wrap
19 changes: 3 additions & 16 deletions mypyc/irbuild/expression.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@
)
from mypyc.irbuild.ast_helpers import is_borrow_friendly_expr, process_conditional
from mypyc.irbuild.builder import IRBuilder, int_borrow_friendly_op
from mypyc.irbuild.constant_fold import constant_fold_expr
from mypyc.irbuild.constant_fold import constant_fold_expr, folding_candidate, try_constant_fold
from mypyc.irbuild.for_helpers import (
comprehension_helper,
raise_error_if_contains_unreachable_names,
Expand Down Expand Up @@ -527,11 +527,8 @@ def translate_cast_expr(builder: IRBuilder, expr: CastExpr) -> Value:
# Operators


@folding_candidate
def transform_unary_expr(builder: IRBuilder, expr: UnaryExpr) -> Value:
folded = try_constant_fold(builder, expr)
if folded:
return folded

return builder.unary_op(builder.accept(expr.expr), expr.op, expr.line)


Expand Down Expand Up @@ -582,6 +579,7 @@ def try_optimize_int_floor_divide(builder: IRBuilder, expr: OpExpr) -> OpExpr:
return expr


@folding_candidate
def transform_index_expr(builder: IRBuilder, expr: IndexExpr) -> Value:
index = expr.index
base_type = builder.node_type(expr.base)
Expand All @@ -604,17 +602,6 @@ def transform_index_expr(builder: IRBuilder, expr: IndexExpr) -> Value:
)


def try_constant_fold(builder: IRBuilder, expr: Expression) -> Value | None:
"""Return the constant value of an expression if possible.

Return None otherwise.
"""
value = constant_fold_expr(builder, expr)
if value is not None:
return builder.load_literal_value(value)
return None


def try_gen_slice_op(builder: IRBuilder, base: Value, index: SliceExpr) -> Value | None:
"""Generate specialized slice op for some index expressions.

Expand Down
31 changes: 31 additions & 0 deletions mypyc/test-data/irbuild-constant-fold.test
Original file line number Diff line number Diff line change
Expand Up @@ -478,3 +478,34 @@ L0:
r3 = (-1.5+2j)
neg_2 = r3
return 1

[case testIndexExprConstantFolding]
from typing import Final

long_string: Final = "long string"

def pos_index() -> None:
a = long_string[5]
def neg_index() -> None:
a = long_string[-5]
def slice_index() -> None:
a = long_string[5:]
[out]
def pos_index():
r0, a :: str
L0:
r0 = 's'
a = r0
return 1
def neg_index():
r0, a :: str
L0:
r0 = 't'
a = r0
return 1
def slice_index():
r0, a :: str
L0:
r0 = 'string'
a = r0
return 1
9 changes: 8 additions & 1 deletion mypyc/test-data/run-strings.test
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def test_unicode() -> None:
assert ne("\U0001f4a9foo", "\U0001f4a8foo" + str())

[case testStringOps]
from typing import List, Optional, Tuple
from typing import Final, List, Optional, Tuple
from testutil import assertRaises

def do_split(s: str, sep: Optional[str] = None, max_split: Optional[int] = None) -> List[str]:
Expand Down Expand Up @@ -226,6 +226,12 @@ def contains(s: str, o: str) -> bool:
def getitem(s: str, index: int) -> str:
return s[index]

final_string: Final = "abc"
final_int: Final = 1

def getitem_folded() -> str:
return final_string[final_int] + final_string[-1]

def find(s: str, substr: str, start: Optional[int] = None, end: Optional[int] = None) -> int:
if start is not None:
if end is not None:
Expand Down Expand Up @@ -263,6 +269,7 @@ def test_getitem() -> None:
getitem(s, 4)
with assertRaises(IndexError, "string index out of range"):
getitem(s, -4)
assert getitem_folded() == "bc"

def test_find() -> None:
s = "abcab"
Expand Down