Skip to content

[mypyc] Fix native int shifts by negative or too large counts - #22419

Open
Jahongir-Qurbonov wants to merge 3 commits into
python:masterfrom
Jahongir-Qurbonov:mypyc-native-int-shift
Open

Jahongir-Qurbonov wants to merge 3 commits into
python:masterfrom
Jahongir-Qurbonov:mypyc-native-int-shift

Conversation

@Jahongir-Qurbonov

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

Copy link
Copy Markdown
Contributor

Shifts of native ints (i64, i32, i16, u32, u16, u8) were compiled to a plain C shift. In C that is undefined behavior when the count is negative or at least the bit width; on x86 the count is effectively masked:

from mypy_extensions import i64, u32

def rshift(x: i64, n: i64) -> i64:
    return x >> n

def lshift(x: u32, n: u32) -> u32:
    return x << n

rshift(-8, 64)  # -8, expected -1
rshift(5, 64)   # 5, expected 0
rshift(8, -1)   # 0, expected ValueError: negative shift count
lshift(1, 32)   # 1, expected 0

With a constant count (e.g. x >> 64 for i64, or x << 40 for i32), the generated C failed to compile ("shift count >= width of type" with -Werror).

Now the plain shift is only used when the count is in range:

  • A negative count raises ValueError.
  • A count at or above the width gives 0 for left shifts and for right shifts of non-negative or unsigned values, and -1 for a right shift of a negative signed value. This matches int (truncated or wrapped, like other native int shifts).
  • Constant counts are resolved at compile time.
  • A variable count only adds one unsigned comparison on the fast path, similar to the division-by-zero check for unsigned types.

This also fixes two related codegen issues:

  • An integer literal on the left of a shift is now cast to the native int type in C. The type of a C shift is the promoted type of the left operand, so 3 << n with n: i64 was a 32-bit shift.
  • Unsigned comparisons of i16 values were missing the uint16_t cast.

Added run tests for all native int types. They cover counts 0, width-1, width, width+1, 64, 100 and negative counts, both constant and variable.

Since this PR was opened, u64 (#22384) and i8 (#22447) were added to master, and they had the same problem (e.g. x >> n with x: u64 = 2**64 - 1, n: u64 = 64 gave 2**64 - 1 instead of 0, and x << n with x = 1, n = 64 gave 1). After merging master, both types go through the same shift lowering. For i8, the unsigned range check needed a uint8_t cast, just like the uint16_t cast for i16. Without it, a negative i8 count passed the check and the C code shifted by a negative count instead of raising ValueError. I added the same run tests to run-u64.test and run-i8.test, plus irbuild tests for a constant u64 count and a variable i8 count.

Shifts of native ints (i64, i32, i16, u32, u16, u8) were compiled to a
plain C shift, which is undefined behavior if the count is negative or
at least the bit width. In practice the count was masked, so e.g.
i64 `5 >> 64` gave 5 and `8 >> -1` gave 0, and a constant count such as
`x >> 64` failed the C build with -Werror.

Use the plain shift only if the count is in range. Otherwise a negative
count raises ValueError, a left shift gives 0 and a right shift gives 0,
or -1 for a negative signed value. Constant counts are resolved at
compile time. The common case needs one extra unsigned comparison.

Also cast an integer literal left operand to the native int type in C,
since the C shift type follows the promoted left operand (`3 << n` for
i64 was a 32-bit shift), and add the missing uint16_t cast for unsigned
comparisons of i16 values.
@JukkaL

JukkaL commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator

I have an alternative implementation of this in a local branch, which uses a a bit different approach. However, it depends on some other changes and won't be ready for a PR yet. I'm currently leaning towards my implementation, since it avoids raising an exception (but it doesn't match CPython semantics). I still need to think about this.

@Jahongir-Qurbonov

Copy link
Copy Markdown
Contributor Author

Thanks, makes sense. Feel free to close this PR if you go with your implementation. If it helps, the tests in this PR may still be useful for checking the edge cases (negative and too large shift counts for each native int type).

The u64 and i8 types were added after the shift fix was written. With
master merged in, their shifts go through the same range-checked
lowering, but for i8 the unsigned range check was emitted without a
uint8_t cast. A negative i8 count was then promoted to a negative int,
passed the check and was used as a C shift count (undefined behavior)
instead of raising ValueError.

Add the missing uint8_t cast for unsigned comparisons of i8 values, and
add shift run tests for u64 and i8 plus irbuild tests, mirroring the
existing ones for the other native int types.
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.

2 participants