Repository: typst/typst
File: crates/typst-library/src/foundations/int.rs (Rust)

Code before:
}

pub fn convert_float_to_int(f: f64) -> StrResult<i64> {
    if f <= i64::MIN as f64 - 1.0 || f >= i64::MAX as f64 + 1.0 {
        Err(eco_format!("number too large"))
    } else {
        Ok(f as i64)
    }
}

#[cold]
fn parse_str_error(kind: &IntErrorKind, span: Span) -> EcoVec<SourceDiagnostic> {
    let error = match kind {
        IntErrorKind::Empty => "empty string isn't a valid integer",
        IntErrorKind::InvalidDigit => "invalid digit found while parsing integer",
        IntErrorKind::PosOverflow => {
            "the integer is too large to fit into a signed 64-bit integer"
        }
        IntErrorKind::NegOverflow => {
            "the integer is too small to fit into a signed 64-bit integer"
        }
        IntErrorKind::Zero => unreachable!(),
        _ => "unhandled number parsing error",
    };

    eco_vec![SourceDiagnostic::error(span, eco_format!("invalid integer: {error}"))]
}

macro_rules! signed_int {
    ($($ty:ty)*) => {
        $(cast! {
            $ty,
            self => {
                #[allow(irrefutable_let_patterns)]
                if let Ok(int) = i64::try_from(self) {
                    Value::Int(int)
                } else {
                    // Some numbers (i128) are too large to be cast as i64
                    // In that case, we accept that there may be a
                    // precision loss, and use a floating point number
                    Value::Float(self as _)
                }
            },
            v: i64 => v.try_into().map_err(|_| "number too large")?,
        })*
    }
}

macro_rules! unsigned_int {
    ($($ty:ty)*) => {
        $(cast! {

Review comment (bug):
This is an improvement, but I think it could be worded better by using hints instead of repeating the error after "invalid integer." We can also pass in the `base` to get a better error for non-decimal bases.

Here's an idea for what that might look like:
(feel free to use this and edit if you dislike any of the wordings)
```rust
#[cold]
fn parse_str_error(
    kind: &IntErrorKind,
    base: Base,
    span: Span,
) -> EcoVec<SourceDiagnostic> {
    let error = match kind {
        IntErrorKind::Empty if base.value() == 10 => error!(span, "expected an integer"),
        IntErrorKind::Empty => error!(span, "expected a base {} integer", base.value()),
        IntErrorKind::InvalidDigit if base.value() == 10 => {
            error!(span, "invalid digits for an integer")
        }
        IntErrorKind::InvalidDigit => {
            error!(span, "invalid digits for a base {} integer", base.value())
        }
        IntErrorKind::PosOverflow => error!(
            span, "invalid integer";
            hint: "number is too large to fit in a signed 64-bit integer";
            hint: "try using a floating point number",
        ),
        IntErrorKind::NegOverflow => error!(
            span, "invalid integer";
            hint: "number is too small to fit in a signed 64-bit integer";
            hint: "try using a floating point number",
        ),
        IntErrorKind::Zero => unreachable!(),
        _ => error!(span, "invalid integer"),
    };
    eco_vec![error]
}
```

An example diff of the test cases would look like:
```diff
 --- int-constructor-str-empty paged ---
-// Error: 6-8 invalid integer: empty string isn't a valid integer
+// Error: 6-8 expected an integer
 #int("")
 
 --- int-constructor-str-empty-base-16 paged ---
-// Error: 6-8 invalid integer: empty string isn't a valid integer
+// Error: 6-8 expected a base 16 integer
 #int("", base: 16)
 
 --- int-constructor-bad-value paged ---
-// Error: 6-12 invalid integer: invalid digit found while parsing integer
+// Error: 6-12 invalid digits for an integer
 #int("nope")
 
 --- int-constructor-bad-value-base-8 paged ---
-// Error: 6-11 invalid integer: invalid digit found while parsing integer
+// Error: 6-11 invalid digits for a base 8 integer
 #int("888", base: 8)
 
 --- int-constructor-str-too-large paged ---
-// Error: 6-27 invalid integer: the integer is too large to fit into a signed 64-bit integer
+// Error: 6-27 invalid integer
+// Hint: 6-27 number is too large to fit in a signed 64-bit integer
+// Hint: 6-27 try using a floating point number
 #int("9223372036854775808")
 
 --- int-constructor-str-too-small paged ---
-// Error: 6-28 invalid integer: the integer is too small to fit into a signed 64-bit integer
+// Error: 6-28 invalid integer
+// Hint: 6-28 number is too small to fit in a signed 64-bit integer
+// Hint: 6-28 try using a floating point number
 #int("-9223372036854775809")
```


Code after:
    v: f64 => Self::Int(convert_float_to_int(v)?),
    v: Decimal => Self::Int(i64::try_from(v).map_err(|_| eco_format!("number too large"))?),
    v: Str => Self::Str(v),
}

pub fn convert_float_to_int(f: f64) -> StrResult<i64> {
    if f <= i64::MIN as f64 - 1.0 || f >= i64::MAX as f64 + 1.0 {
        Err(eco_format!("number too large"))
    } else {
        Ok(f as i64)
    }
}

#[cold]
fn parse_str_error(kind: &IntErrorKind, base: Spanned<Base>) -> HintedString {
    let base = base.v.value();
    match kind {
        IntErrorKind::Empty => error!("string must not be empty"),
        IntErrorKind::InvalidDigit => match base {
            10 => error!("string contains invalid digits"),
            _ => error!("string contains invalid digits for a base {base} integer"),
        },
        IntErrorKind::PosOverflow => error!(
            "integer value is too large";
            hint: "value does not fit into a signed 64-bit integer";
            hint: "try using a floating point number"
        ),
        IntErrorKind::NegOverflow => error!(
            "integer value is too small";
            hint: "value does not fit into a signed 64-bit integer";
            hint: "try using a floating point number"
        ),
        IntErrorKind::Zero => unreachable!(),
        _ => error!("invalid integer"),
    }
}

macro_rules! signed_int {
    ($($ty:ty)*) => {
        $(cast! {
            $ty,
            self => {
                #[allow(irrefutable_let_patterns)]
                if let Ok(int) = i64::try_from(self) {
                    Value::Int(int)
                } else {
                    // Some numbers (i128) are too large to be cast as i64
                    // In that case, we accept that there may be a
                    // precision loss, and use a floating point number
                    Value::Float(self as _)
                }