Skip to content

Commit d871b03

Browse files
committed
Do not crash when folding constants that overflow
1 parent 5bb72b7 commit d871b03

4 files changed

Lines changed: 80 additions & 19 deletions

File tree

mypy/constant_fold.py

Lines changed: 32 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,9 @@ def constant_fold_binary_op(
112112

113113

114114
def constant_fold_binary_int_op(op: str, left: int, right: int) -> int | float | None:
115+
# Operands are unbounded ints, so some results do not fit into a float (`/`) or
116+
# cannot be built at all (`<<` with a huge count). Folding is an optimization:
117+
# when it cannot produce a value, return None and let the expression stand.
115118
if op == "+":
116119
return left + right
117120
if op == "-":
@@ -120,7 +123,10 @@ def constant_fold_binary_int_op(op: str, left: int, right: int) -> int | float |
120123
return left * right
121124
elif op == "/":
122125
if right != 0:
123-
return left / right
126+
try:
127+
return left / right
128+
except OverflowError:
129+
return None
124130
elif op == "//":
125131
if right != 0:
126132
return left // right
@@ -135,7 +141,10 @@ def constant_fold_binary_int_op(op: str, left: int, right: int) -> int | float |
135141
return left ^ right
136142
elif op == "<<":
137143
if right >= 0:
138-
return left << right
144+
try:
145+
return left << right
146+
except (OverflowError, ValueError):
147+
return None
139148
elif op == ">>":
140149
if right >= 0:
141150
return left >> right
@@ -149,22 +158,27 @@ def constant_fold_binary_int_op(op: str, left: int, right: int) -> int | float |
149158

150159
def constant_fold_binary_float_op(op: str, left: int | float, right: int | float) -> float | None:
151160
assert not (isinstance(left, int) and isinstance(right, int)), (op, left, right)
152-
if op == "+":
153-
return left + right
154-
elif op == "-":
155-
return left - right
156-
elif op == "*":
157-
return left * right
158-
elif op == "/":
159-
if right != 0:
160-
return left / right
161-
elif op == "//":
162-
if right != 0:
163-
return left // right
164-
elif op == "%":
165-
if right != 0:
166-
return left % right
167-
elif op == "**":
161+
# An int operand here is unbounded, so converting it to a float can overflow.
162+
# `**` already guards against this; the other operations get the same treatment.
163+
try:
164+
if op == "+":
165+
return left + right
166+
elif op == "-":
167+
return left - right
168+
elif op == "*":
169+
return left * right
170+
elif op == "/":
171+
if right != 0:
172+
return left / right
173+
elif op == "//":
174+
if right != 0:
175+
return left // right
176+
elif op == "%":
177+
if right != 0:
178+
return left % right
179+
except OverflowError:
180+
return None
181+
if op == "**":
168182
if (left < 0 and isinstance(right, int)) or left > 0:
169183
try:
170184
ret = left**right

mypy/test/testconstantfold.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
"""Tests for constant folding of huge operands."""
2+
3+
from __future__ import annotations
4+
5+
from mypy.constant_fold import constant_fold_binary_float_op, constant_fold_binary_int_op
6+
from mypy.test.helpers import Suite
7+
8+
BIG = 2**2000
9+
10+
11+
class ConstantFoldOverflowSuite(Suite):
12+
"""Folding is an optimization: when a result cannot be built, it must yield None."""
13+
14+
def test_int_div_overflow(self) -> None:
15+
assert constant_fold_binary_int_op("/", BIG, 3) is None
16+
17+
def test_int_lshift_huge_count(self) -> None:
18+
assert constant_fold_binary_int_op("<<", 1, 2**70) is None
19+
20+
def test_float_ops_with_huge_int(self) -> None:
21+
for op in ("+", "-", "*", "/", "//", "%"):
22+
assert constant_fold_binary_float_op(op, BIG, 1.0) is None, op
23+
24+
def test_small_operands_still_fold(self) -> None:
25+
assert constant_fold_binary_int_op("/", 6, 3) == 2.0
26+
assert constant_fold_binary_int_op("<<", 1, 4) == 16
27+
assert constant_fold_binary_float_op("+", 1, 2.5) == 3.5
28+
assert constant_fold_binary_float_op("**", 2.0, 3) == 8.0

mypy/test/testtypes.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,19 @@
6161
import mypy.expandtype # ruff: isort: skip
6262

6363

64+
class LiteralTypeReprSuite(Suite):
65+
def setUp(self) -> None:
66+
self.fx = TypeFixture()
67+
68+
def test_value_repr_of_huge_int(self) -> None:
69+
# repr() of an int is limited by sys.set_int_max_str_digits(); a literal built
70+
# from a folded power can exceed it and used to raise ValueError.
71+
huge = LiteralType(2**100000, self.fx.a)
72+
rendered = huge.value_repr()
73+
assert rendered.startswith("0x")
74+
assert int(rendered, 16) == 2**100000
75+
76+
6477
class TypesSuite(Suite):
6578
def setUp(self) -> None:
6679
self.x = UnboundType("X") # Helpers

mypy/types.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3396,7 +3396,13 @@ def value_repr(self) -> str:
33963396
if isinstance(self.value, SentinelValue):
33973397
return self.value.name
33983398

3399-
raw = repr(self.value)
3399+
try:
3400+
raw = repr(self.value)
3401+
except ValueError:
3402+
# int -> str conversion is limited by sys.set_int_max_str_digits(); a literal
3403+
# type built from a folded power can exceed it. Fall back to a lossless form.
3404+
assert isinstance(self.value, int)
3405+
raw = hex(self.value)
34003406
fallback_name = self.fallback.type.fullname
34013407

34023408
# If this is backed by an enum,

0 commit comments

Comments
 (0)