Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -1052,7 +1052,6 @@ jobs:
WASM_WASI_SNIPPET_EXCLUDE: >-
builtin_bytearray
builtin_hash
builtin_int
builtin_memoryview
builtin_open
builtin_posixshmem
Expand Down
13 changes: 10 additions & 3 deletions crates/vm/src/builtins/int.rs
Original file line number Diff line number Diff line change
Expand Up @@ -211,17 +211,24 @@ fn inner_lshift(base: &BigInt, bits: &BigInt, vm: &VirtualMachine) -> PyResult {
bits,
|base, bits| base << bits,
|bits, vm| {
// CPython's limit: `(i64::MAX - 1) / 30` digits of 30 bits.
const MAX_DIGITS: u128 = (i64::MAX as u128 - 1) / 30;
// Match CPython's MAX_LONG_DIGITS for 30-bit, u32 digits. On
// 32-bit platforms the allocation, including the reference count,
// type pointer and digit-count tag, must fit in Py_ssize_t.
const MAX_DIGITS: u128 = if usize::BITS < 64 {
(isize::MAX as u128 - 3 * size_of::<usize>() as u128) / size_of::<u32>() as u128
} else {
(i64::MAX as u128 - 1) / 30
};
let digits = bits.to_u128().map(|shift| {
u128::from(base.bits().div_ceil(30)) + shift / 30 + u128::from(shift % 30 != 0)
});
if digits.is_none_or(|digits| digits > MAX_DIGITS) {
return Err(vm.new_overflow_error("too many digits in integer"));
}
let shift = bits.to_u64().ok_or_else(|| vm.no_memory_error())?;
let shift_bits = usize::try_from(shift).map_err(|_| vm.no_memory_error())?;
reserve_result_bits(base.bits().checked_add(shift), vm)?;
usize::try_from(shift).map_err(|_| vm.no_memory_error())
Ok(shift_bits)
},
vm,
)
Expand Down
29 changes: 29 additions & 0 deletions extra_tests/snippets/builtin_int.py
Original file line number Diff line number Diff line change
Expand Up @@ -427,3 +427,32 @@ class SubInt(int):
assert_raises(OverflowError, lambda: -5 << 10**30)
assert 0 << 2**64 == 0
assert pow(1, 2**63) == 1 and pow(-1, 2**63 + 1) == -1 and pow(0, 2**63) == 0

# Check the allocation boundary itself, including a carry digit for a partial
# shift and the digits already occupied by the left operand. CPython's 32-bit
# PyLong header has three pointer-sized fields before its u32 digit array.
_digit_bits = sys.int_info.bits_per_digit
_max_digits = (
(sys.maxsize - 12) // sys.int_info.sizeof_digit
if sys.maxsize < 2**32
else (2**63 - 2) // _digit_bits
)
for _base in (1, -1, 2**30, -(2**30), 2**62, -(2**62)):
_base_digits = (_base.bit_length() + _digit_bits - 1) // _digit_bits
_last_shift = (_max_digits - _base_digits) * _digit_bits
# On 32-bit CPython this could actually allocate approximately 2 GiB.
if sys.maxsize > 2**32:
with assert_raises(MemoryError):
_base << _last_shift
for _shift in (_last_shift + 1, _last_shift + _digit_bits, 2**63, 2**128):
with assert_raises(OverflowError) as _error:
_base << _shift
assert str(_error.exception) == "too many digits in integer"

# Check the negative-count message even when zero or overflow could otherwise
# short-circuit the shift.
for _shift in (_max_digits * _digit_bits, 2**128):
for _base in (0, 1, -1):
with assert_raises(ValueError) as _error:
_base << -_shift
assert str(_error.exception) == "negative shift count"
Loading