Skip to content

[mypyc] Fix negation and constant folding of unsigned native int constants - #22469

Open
Jahongir-Qurbonov wants to merge 3 commits into
python:masterfrom
Jahongir-Qurbonov:mypyc-unsigned-literal-negation
Open

Jahongir-Qurbonov wants to merge 3 commits into
python:masterfrom
Jahongir-Qurbonov:mypyc-unsigned-literal-negation

Conversation

@Jahongir-Qurbonov

@Jahongir-Qurbonov Jahongir-Qurbonov commented Nov 14, 2026 •

Copy link
Copy Markdown
Contributor

Unary minus on an integer literal is constant folded in LowLevelIRBuilder.unary_minus, but the folded value wasn't wrapped around for unsigned native int types (there was a # TODO: Overflow? Unsigned? comment). So -u64(1) became the u64 constant -1 instead of 2**64 - 1:

from mypy_extensions import u64, u8

def f() -> int:
    return -u64(1)  # -1, expected 18446744073709551615

def g() -> int:
    return -u8(1)  # -1, expected 255

def h(x: u64) -> bool:
    return x == -u64(1)  # C compile error: -Werror=sign-compare

str(-u64(1)) also gave "-1", and comparisons or other operations with a negated unsigned literal could produce C code that fails to compile with -Werror=sign-compare. u64, u32, u16 and u8 were all affected.

Now the folded value is taken modulo 2**bits for unsigned types, which matches what negating a non-literal unsigned value does at runtime (and the docs say unsigned operations wrap around on overflow). Large u64 values are already emitted with a ULL suffix. Signed overflow (e.g. -i64(-2**63)) is left alone, since it's undefined behavior per the docs.

I checked related cases like 0 - u64(1) and ~u64(0); they don't go through this folding and already worked, so I only added run tests for them.

Added a test_negate_literal run test to run-u8/u16/u32/u64.test, and irbuild tests for u8 and u64 that show the folded constants.

Negating a Final unsigned native int constant was also rejected, e.g. -X with X: Final[u64] = 1 gave Value -1 is out of range for "u64" (likewise X - 2, ~X, X + 1 for X: Final[u8] = 255, and references to Z: Final[u64] = -X), and "%d" % (-X,) silently produced "-1". Constant folding and Final value loading now wrap int results around for unsigned native int types, at each folded operation, so the results match runtime semantics (e.g. (X - 2) // 2 is 2**63 - 1). Signed native int overflow is undefined behavior, so overflowing signed constant expressions are still reported as errors.

Converting such a constant to int (e.g. int(-u32(1))) generated an unreachable fast path that truncated the constant, which C compilers reject on 32-bit platforms, so large native int constants are now loaded directly as int literals in that case.

Unary minus on an integer literal is constant folded in
LowLevelIRBuilder.unary_minus, but the result wasn't wrapped around
for unsigned native int types. For example, -u64(1) was folded into
the u64 constant -1 instead of 2**64 - 1, so int(-u64(1)) and
str(-u64(1)) produced -1, and comparisons such as x == -u64(1) emitted
C code that failed to compile with -Werror=sign-compare. The same
happened with u32, u16 and u8.

Wrap the folded value modulo 2**bits for unsigned types, which matches
the behavior of negating a non-literal unsigned value at runtime.
Constant folding evaluated operations on Final native int values using
arbitrary-precision int semantics, so expressions such as "-X", "X - 2"
or "~X" with "X: Final[u64] = 1" produced a negative value that was then
rejected with 'Value -1 is out of range for "u64"'. Similar errors
happened for u8/u16/u32 (e.g. "X + 1" with "X: Final[u8] = 255"), and
"%d" % (-X,) silently produced "-1". At runtime unsigned native int
operations wrap around.

Wrap each folded int result around based on the native int type of the
expression, so that intermediate results also behave like at runtime
(e.g. (X - 2) // 2). mypy also folds Final initializers like
"Y: Final[u64] = -X" into the out-of-range final value -1, so also wrap
final values when loading them.

Signed native int overflow is undefined behavior, so folded expressions
that overflow a signed type are still reported as errors.
@Jahongir-Qurbonov Jahongir-Qurbonov changed the title [mypyc] Fix negation of unsigned native int literals [mypyc] Fix negation and constant folding of unsigned native int constants Nov 14, 2026
…32-bit

Converting a native int constant that doesn't fit in a short int, such as
-u32(1) or -u64(1), to int generated an unreachable fast path that
truncated or shifted the constant. On 32-bit platforms the C compiler
rejected this with -Werror=overflow. Load such constants as int literals
instead.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant