Implement our own small-integer optimization (#7584)

## Summary

This is a follow-up to #7469 that attempts to achieve similar gains, but
without introducing malachite. Instead, this PR removes the `BigInt`
type altogether, instead opting for a simple enum that allows us to
store small integers directly and only allocate for values greater than
`i64`:

```rust
/// A Python integer literal. Represents both small (fits in an `i64`) and large integers.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Int(Number);

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Number {
    /// A "small" number that can be represented as an `i64`.
    Small(i64),
    /// A "large" number that cannot be represented as an `i64`.
    Big(Box<str>),
}

impl std::fmt::Display for Number {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Number::Small(value) => write!(f, "{value}"),
            Number::Big(value) => write!(f, "{value}"),
        }
    }
}
```

We typically don't care about numbers greater than `isize` -- our only
uses are comparisons against small constants (like `1`, `2`, `3`, etc.),
so there's no real loss of information, except in one or two rules where
we're now a little more conservative (with the worst-case being that we
don't flag, e.g., an `itertools.pairwise` that uses an extremely large
value for the slice start constant). For simplicity, a few diagnostics
now show a dedicated message when they see integers that are out of the
supported range (e.g., `outdated-version-block`).

An additional benefit here is that we get to remove a few dependencies,
especially `num-bigint`.

## Test Plan

`cargo test`
This commit is contained in:
Charlie Marsh 2023-09-25 11:13:21 -04:00 committed by GitHub
parent 65aebf127a
commit 93b5d8a0fb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 707 additions and 385 deletions

View file

@ -0,0 +1,228 @@
use std::fmt::Debug;
use std::str::FromStr;
/// A Python integer literal. Represents both small (fits in an `i64`) and large integers.
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct Int(Number);
impl FromStr for Int {
type Err = std::num::ParseIntError;
/// Parse an [`Int`] from a string.
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.parse::<i64>() {
Ok(value) => Ok(Int::small(value)),
Err(err) => {
if matches!(
err.kind(),
std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
) {
Ok(Int::big(s))
} else {
Err(err)
}
}
}
}
}
impl Int {
pub const ZERO: Int = Int(Number::Small(0));
pub const ONE: Int = Int(Number::Small(1));
/// Create an [`Int`] to represent a value that can be represented as an `i64`.
fn small(value: i64) -> Self {
Self(Number::Small(value))
}
/// Create an [`Int`] to represent a value that cannot be represented as an `i64`.
fn big(value: impl Into<Box<str>>) -> Self {
Self(Number::Big(value.into()))
}
/// Parse an [`Int`] from a string with a given radix.
pub fn from_str_radix(s: &str, radix: u32) -> Result<Self, std::num::ParseIntError> {
match i64::from_str_radix(s, radix) {
Ok(value) => Ok(Int::small(value)),
Err(err) => {
if matches!(
err.kind(),
std::num::IntErrorKind::PosOverflow | std::num::IntErrorKind::NegOverflow
) {
Ok(Int::big(s))
} else {
Err(err)
}
}
}
}
/// Return the [`Int`] as an u8, if it can be represented as that data type.
pub fn as_u8(&self) -> Option<u8> {
match &self.0 {
Number::Small(small) => u8::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an u16, if it can be represented as that data type.
pub fn as_u16(&self) -> Option<u16> {
match &self.0 {
Number::Small(small) => u16::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an u32, if it can be represented as that data type.
pub fn as_u32(&self) -> Option<u32> {
match &self.0 {
Number::Small(small) => u32::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an i8, if it can be represented as that data type.
pub fn as_i8(&self) -> Option<i8> {
match &self.0 {
Number::Small(small) => i8::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an i16, if it can be represented as that data type.
pub fn as_i16(&self) -> Option<i16> {
match &self.0 {
Number::Small(small) => i16::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an i32, if it can be represented as that data type.
pub fn as_i32(&self) -> Option<i32> {
match &self.0 {
Number::Small(small) => i32::try_from(*small).ok(),
Number::Big(_) => None,
}
}
/// Return the [`Int`] as an i64, if it can be represented as that data type.
pub const fn as_i64(&self) -> Option<i64> {
match &self.0 {
Number::Small(small) => Some(*small),
Number::Big(_) => None,
}
}
}
impl std::fmt::Display for Int {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl Debug for Int {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
std::fmt::Display::fmt(self, f)
}
}
impl PartialEq<u8> for Int {
fn eq(&self, other: &u8) -> bool {
self.as_u8() == Some(*other)
}
}
impl PartialEq<u16> for Int {
fn eq(&self, other: &u16) -> bool {
self.as_u16() == Some(*other)
}
}
impl PartialEq<u32> for Int {
fn eq(&self, other: &u32) -> bool {
self.as_u32() == Some(*other)
}
}
impl PartialEq<i8> for Int {
fn eq(&self, other: &i8) -> bool {
self.as_i8() == Some(*other)
}
}
impl PartialEq<i16> for Int {
fn eq(&self, other: &i16) -> bool {
self.as_i16() == Some(*other)
}
}
impl PartialEq<i32> for Int {
fn eq(&self, other: &i32) -> bool {
self.as_i32() == Some(*other)
}
}
impl PartialEq<i64> for Int {
fn eq(&self, other: &i64) -> bool {
self.as_i64() == Some(*other)
}
}
impl From<u8> for Int {
fn from(value: u8) -> Self {
Self::small(i64::from(value))
}
}
impl From<u16> for Int {
fn from(value: u16) -> Self {
Self::small(i64::from(value))
}
}
impl From<u32> for Int {
fn from(value: u32) -> Self {
Self::small(i64::from(value))
}
}
impl From<i8> for Int {
fn from(value: i8) -> Self {
Self::small(i64::from(value))
}
}
impl From<i16> for Int {
fn from(value: i16) -> Self {
Self::small(i64::from(value))
}
}
impl From<i32> for Int {
fn from(value: i32) -> Self {
Self::small(i64::from(value))
}
}
impl From<i64> for Int {
fn from(value: i64) -> Self {
Self::small(value)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum Number {
/// A "small" number that can be represented as an `i64`.
Small(i64),
/// A "large" number that cannot be represented as an `i64`.
Big(Box<str>),
}
impl std::fmt::Display for Number {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Number::Small(value) => write!(f, "{value}"),
Number::Big(value) => write!(f, "{value}"),
}
}
}