Repository navigation
[mypyc] Coerce between signed and unsigned native ints without boxing - #22471
Open
Jahongir-Qurbonov wants to merge 3 commits into
Open
Jahongir-Qurbonov wants to merge 3 commits into
Jahongir-Qurbonov wants to merge 3 commits into
Conversation
LowLevelIRBuilder.coerce only had a direct path between native ints of the same signedness. Coercing an unsigned native int (u8, u16, u32, u64) to a signed one, for example when indexing bytes with a u64 (the index primitives take an i64), fell through to the generic box/unbox path and allocated a Python int every time. Zero-extend when the target type is wider. Otherwise range check the value against the target's maximum and truncate. Values that don't fit raise the same ValueError as before; add CPyInt64_Overflow for the u64 -> i64 case, with the same message as i64 unboxing.
Coercing a signed native int (i8, i16, i32, i64) to an unsigned one,
for example via cast(u64, x) where x is an i64, still went through the
generic box/unbox path and allocated a Python int every time.
Add the reverse of the unsigned -> signed direct path: check that the
value is non-negative and, if the target type is narrower than the
source type, that it doesn't exceed the target's maximum. Then
zero-extend or truncate. Values that don't fit raise the same
ValueError as unboxing ("int too large or small to convert to uN").
Signed comparisons are used, since the C backend doesn't emit unsigned
casts for i8/i16 operands of unsigned comparison ops.
Indexing bytes, or str in `ord(s[i])` and `s[i] == "x"`, uses specialized primitives that take an i64 index. A u64 index was coerced to i64, so a value >= 2**63 raised "ValueError: int too large to convert to i64", while CPython raises IndexError. Such a value is always out of range, so branch to the existing IndexError block instead of coercing. This replaces the overflow check emitted by the coercion, so there is no additional overhead. Smaller unsigned types always fit in i64 and are unaffected.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
LowLevelIRBuilder.coerceonly had a direct path between native ints ofthe same signedness. Coercing an unsigned native int (
u8,u16,u32,u64) to a signed one fell through to the generic box/unbox path, so aPython int was allocated and freed every time. This happens when an
unsigned value is passed to a primitive that takes
i64, for exampleindexing bytes:
Before this change, the IR for
fstarted withr0 = box(u64, i); r1 = unbox(i64, r0), andu8/u16/u32indexesdid the same.
Now the coercion is done inline. If the target type is wider, it's a
zero-extending
Extend. Otherwise the value is range checked againstthe target's maximum and then truncated. Values that don't fit raise the
same
ValueErroras before: theu64->i64overflow uses a newCPyInt64_Overflowhelper with the existing "int too large to convertto i64" message. This is an implicit coercion, so it has to keep the
value. It doesn't wrap around like an explicit
i64(x)call.This also adds the reverse direct path, signed → unsigned (e.g.
cast(u64, x)withx: i64,cast(u8, x)withx: i32), which also went through box/unbox before. The value is checked to be non-negative, and when the source type is wider than the target, also checked against the target's maximum; then it's zero-extended or truncated. Out-of-range values raise the sameValueError("int too large or small to convert to uN")as unboxing, using the existingCPyUIntN_Overflowhelpers.Indexing
bytes, orstrinord(s[i])ands[i] == "x", with au64index >= 2**63 raisedValueError: int too large to convert to i64(from the coercion to thei64index the specialized primitives take), while CPython raisesIndexError. Such a value is always out of range, so it now branches to the existingIndexErrorblock instead; this replaces the overflow check emitted by the coercion, so there's no extra overhead. Smaller unsigned types always fit ini64and are unaffected.I added irbuild tests for
bytesindexing withu64andu8indexes,and run tests for
u8,u32andu64indexes that cover edge values:in range, just out of range,
2**63 - 1,2**63and2**64 - 1.The later commits add irbuild tests for
cast()from signed to unsigned types and run tests covering boundaries (0, -1, the target's maximum and maximum + 1, the source's minimum and maximum) for several width combinations, andbytes/strindex tests now expectIndexErrorforu64values >= 2**63.