From d871b03e777d6ecba62863b16c363055a64aed12 Mon Sep 17 00:00:00 2001 From: Eljees <3.14hell@gmail.com> Date: Fri, 7 Aug 2026 12:55:27 +0000 Subject: [PATCH] Do not crash when folding constants that overflow --- mypy/constant_fold.py | 50 ++++++++++++++++++++++------------- mypy/test/testconstantfold.py | 28 ++++++++++++++++++++ mypy/test/testtypes.py | 13 +++++++++ mypy/types.py | 8 +++++- 4 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 mypy/test/testconstantfold.py diff --git a/mypy/constant_fold.py b/mypy/constant_fold.py index e1b915df22985..07503237cf043 100644 --- a/mypy/constant_fold.py +++ b/mypy/constant_fold.py @@ -112,6 +112,9 @@ def constant_fold_binary_op( def constant_fold_binary_int_op(op: str, left: int, right: int) -> int | float | None: + # Operands are unbounded ints, so some results do not fit into a float (`/`) or + # cannot be built at all (`<<` with a huge count). Folding is an optimization: + # when it cannot produce a value, return None and let the expression stand. if op == "+": return left + right if op == "-": @@ -120,7 +123,10 @@ def constant_fold_binary_int_op(op: str, left: int, right: int) -> int | float | return left * right elif op == "/": if right != 0: - return left / right + try: + return left / right + except OverflowError: + return None elif op == "//": if right != 0: return left // right @@ -135,7 +141,10 @@ def constant_fold_binary_int_op(op: str, left: int, right: int) -> int | float | return left ^ right elif op == "<<": if right >= 0: - return left << right + try: + return left << right + except (OverflowError, ValueError): + return None elif op == ">>": if right >= 0: return left >> right @@ -149,22 +158,27 @@ def constant_fold_binary_int_op(op: str, left: int, right: int) -> int | float | def constant_fold_binary_float_op(op: str, left: int | float, right: int | float) -> float | None: assert not (isinstance(left, int) and isinstance(right, int)), (op, left, right) - if 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: - return left % right - elif op == "**": + # An int operand here is unbounded, so converting it to a float can overflow. + # `**` already guards against this; the other operations get the same treatment. + try: + if 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: + return left % right + except OverflowError: + return None + if op == "**": if (left < 0 and isinstance(right, int)) or left > 0: try: ret = left**right diff --git a/mypy/test/testconstantfold.py b/mypy/test/testconstantfold.py new file mode 100644 index 0000000000000..c0af59ab48e36 --- /dev/null +++ b/mypy/test/testconstantfold.py @@ -0,0 +1,28 @@ +"""Tests for constant folding of huge operands.""" + +from __future__ import annotations + +from mypy.constant_fold import constant_fold_binary_float_op, constant_fold_binary_int_op +from mypy.test.helpers import Suite + +BIG = 2**2000 + + +class ConstantFoldOverflowSuite(Suite): + """Folding is an optimization: when a result cannot be built, it must yield None.""" + + def test_int_div_overflow(self) -> None: + assert constant_fold_binary_int_op("/", BIG, 3) is None + + def test_int_lshift_huge_count(self) -> None: + assert constant_fold_binary_int_op("<<", 1, 2**70) is None + + def test_float_ops_with_huge_int(self) -> None: + for op in ("+", "-", "*", "/", "//", "%"): + assert constant_fold_binary_float_op(op, BIG, 1.0) is None, op + + def test_small_operands_still_fold(self) -> None: + assert constant_fold_binary_int_op("/", 6, 3) == 2.0 + assert constant_fold_binary_int_op("<<", 1, 4) == 16 + assert constant_fold_binary_float_op("+", 1, 2.5) == 3.5 + assert constant_fold_binary_float_op("**", 2.0, 3) == 8.0 diff --git a/mypy/test/testtypes.py b/mypy/test/testtypes.py index b287e82b3d4af..cb35a825e69fe 100644 --- a/mypy/test/testtypes.py +++ b/mypy/test/testtypes.py @@ -61,6 +61,19 @@ import mypy.expandtype # ruff: isort: skip +class LiteralTypeReprSuite(Suite): + def setUp(self) -> None: + self.fx = TypeFixture() + + def test_value_repr_of_huge_int(self) -> None: + # repr() of an int is limited by sys.set_int_max_str_digits(); a literal built + # from a folded power can exceed it and used to raise ValueError. + huge = LiteralType(2**100000, self.fx.a) + rendered = huge.value_repr() + assert rendered.startswith("0x") + assert int(rendered, 16) == 2**100000 + + class TypesSuite(Suite): def setUp(self) -> None: self.x = UnboundType("X") # Helpers diff --git a/mypy/types.py b/mypy/types.py index 7a1470964d251..8a4a3972c7da5 100644 --- a/mypy/types.py +++ b/mypy/types.py @@ -3396,7 +3396,13 @@ def value_repr(self) -> str: if isinstance(self.value, SentinelValue): return self.value.name - raw = repr(self.value) + try: + raw = repr(self.value) + except ValueError: + # int -> str conversion is limited by sys.set_int_max_str_digits(); a literal + # type built from a folded power can exceed it. Fall back to a lossless form. + assert isinstance(self.value, int) + raw = hex(self.value) fallback_name = self.fallback.type.fullname # If this is backed by an enum,