Better error spans in calc

This commit is contained in:
Laurenz 2023-04-13 15:13:31 +02:00
parent e11bd2a193
commit 9025ecb2ee
3 changed files with 100 additions and 96 deletions

View File

@ -94,28 +94,27 @@ pub fn pow(
/// The exponent of the power. Must be non-negative. /// The exponent of the power. Must be non-negative.
exponent: Spanned<Num>, exponent: Spanned<Num>,
) -> Value { ) -> Value {
let Spanned { v: exp, span } = exponent; match exponent.v {
match exp { _ if exponent.v.float() == 0.0 && base.float() == 0.0 => {
_ if exp.float() == 0.0 && base.float() == 0.0 => {
bail!(args.span, "zero to the power of zero is undefined") bail!(args.span, "zero to the power of zero is undefined")
} }
Num::Int(i) if i32::try_from(i).is_err() => { Num::Int(i) if i32::try_from(i).is_err() => {
bail!(span, "exponent is too large") bail!(exponent.span, "exponent is too large")
} }
Num::Float(f) if !f.is_normal() && f != 0.0 => { Num::Float(f) if !f.is_normal() && f != 0.0 => {
bail!(span, "exponent may not be infinite, subnormal, or NaN") bail!(exponent.span, "exponent may not be infinite, subnormal, or NaN")
} }
_ => {} _ => {}
}; };
let result = match (base, exp) { let result = match (base, exponent.v) {
(Num::Int(a), Num::Int(b)) if b >= 0 => Num::Int(a.pow(b as u32)), (Num::Int(a), Num::Int(b)) if b >= 0 => Num::Int(a.pow(b as u32)),
(a, Num::Int(b)) => Num::Float(a.float().powi(b as i32)), (a, Num::Int(b)) => Num::Float(a.float().powi(b as i32)),
(a, b) => Num::Float(a.float().powf(b.float())), (a, b) => Num::Float(a.float().powf(b.float())),
}; };
if result.float().is_nan() { if result.float().is_nan() {
bail!(span, "the result is not a real number") bail!(args.span, "the result is not a real number")
} }
result.value() result.value()
@ -381,28 +380,28 @@ pub fn log(
value: Spanned<Num>, value: Spanned<Num>,
/// The base of the logarithm. Defaults to `{10}` and may not be zero. /// The base of the logarithm. Defaults to `{10}` and may not be zero.
#[named] #[named]
#[default(10.0)] #[default(Spanned::new(10.0, Span::detached()))]
base: f64, base: Spanned<f64>,
) -> Value { ) -> Value {
let number = value.v.float(); let number = value.v.float();
if number <= 0.0 { if number <= 0.0 {
bail!(value.span, "value must be strictly positive") bail!(value.span, "value must be strictly positive")
} }
if !base.is_normal() { if !base.v.is_normal() {
bail!(value.span, "base may not be zero, NaN, infinite, or subnormal") bail!(base.span, "base may not be zero, NaN, infinite, or subnormal")
} }
let result = if base == 2.0 { let result = if base.v == 2.0 {
number.log2() number.log2()
} else if base == 10.0 { } else if base.v == 10.0 {
number.log10() number.log10()
} else { } else {
number.log(base) number.log(base.v)
}; };
if result.is_infinite() || result.is_nan() { if result.is_infinite() || result.is_nan() {
bail!(value.span, "the result is not a real number") bail!(args.span, "the result is not a real number")
} }
Value::Float(result) Value::Float(result)
@ -420,32 +419,30 @@ pub fn log(
/// Returns: integer /// Returns: integer
#[func] #[func]
pub fn fact( pub fn fact(
/// The number whose factorial to calculate. Must be positive. /// The number whose factorial to calculate. Must be non-negative.
number: Spanned<u64>, number: u64,
) -> Value { ) -> Value {
let result = factorial_range(1, number.v).and_then(|r| i64::try_from(r).ok()); factorial_range(1, number)
.map(Value::Int)
match result { .ok_or("the result is too large")
None => bail!(number.span, "the factorial result is too large"), .at(args.span)?
Some(s) => Value::Int(s),
}
} }
/// Calculates the product of a range of numbers. Used to calculate permutations. /// Calculates the product of a range of numbers. Used to calculate
/// Returns None if the result is larger than `u64::MAX` /// permutations. Returns None if the result is larger than `i64::MAX`
fn factorial_range(start: u64, end: u64) -> Option<u64> { fn factorial_range(start: u64, end: u64) -> Option<i64> {
// By convention // By convention
if end + 1 < start { if end + 1 < start {
return Some(0); return Some(0);
} }
let mut count: u64 = 1;
let real_start: u64 = cmp::max(1, start); let real_start: u64 = cmp::max(1, start);
let mut count: u64 = 1;
for i in real_start..=end { for i in real_start..=end {
count = count.checked_mul(i)?; count = count.checked_mul(i)?;
} }
Some(count)
i64::try_from(count).ok()
} }
/// Calculate a permutation. /// Calculate a permutation.
@ -460,26 +457,20 @@ fn factorial_range(start: u64, end: u64) -> Option<u64> {
/// Returns: integer /// Returns: integer
#[func] #[func]
pub fn perm( pub fn perm(
/// The base number. Must be positive. /// The base number. Must be non-negative.
base: Spanned<u64>, base: u64,
/// The number of permutations. Must be positive. /// The number of permutations. Must be non-negative.
numbers: Spanned<u64>, numbers: u64,
) -> Value { ) -> Value {
let base_parsed = base.v; // By convention.
let numbers_parsed = numbers.v; if base + 1 <= numbers {
return Ok(Value::Int(0));
let result = if base_parsed + 1 > numbers_parsed {
factorial_range(base_parsed - numbers_parsed + 1, base_parsed)
.and_then(|value| i64::try_from(value).ok())
} else {
// By convention
Some(0)
};
match result {
None => bail!(base.span, "the permutation result is too large"),
Some(s) => Value::Int(s),
} }
factorial_range(base - numbers + 1, base)
.map(Value::Int)
.ok_or("the result is too large")
.at(args.span)?
} }
/// Calculate a binomial coefficient. /// Calculate a binomial coefficient.
@ -489,45 +480,42 @@ pub fn perm(
/// #calc.binom(10, 5) /// #calc.binom(10, 5)
/// ``` /// ```
/// ///
/// Display: Permutation /// Display: Binomial
/// Category: calculate /// Category: calculate
/// Returns: integer /// Returns: integer
#[func] #[func]
pub fn binom( pub fn binom(
/// The upper coefficient. Must be positive /// The upper coefficient. Must be non-negative.
n: Spanned<u64>, n: u64,
/// The lower coefficient. Must be positive. /// The lower coefficient. Must be non-negative.
k: Spanned<u64>, k: u64,
) -> Value { ) -> Value {
let result = binomial(n.v, k.v).and_then(|raw| i64::try_from(raw).ok()); binomial(n, k)
.map(Value::Int)
match result { .ok_or("the result is too large")
None => bail!(n.span, "the binomial result is too large"), .at(args.span)?
Some(r) => Value::Int(r),
}
} }
/// Calculates a binomial coefficient, with `n` the upper coefficient and `k` the lower coefficient. /// Calculates a binomial coefficient, with `n` the upper coefficient and `k`
/// Returns `None` if the result is larger than `u64::MAX` /// the lower coefficient. Returns `None` if the result is larger than
fn binomial(n: u64, k: u64) -> Option<u64> { /// `i64::MAX`
fn binomial(n: u64, k: u64) -> Option<i64> {
if k > n { if k > n {
return Some(0); return Some(0);
} }
// By symmetry // By symmetry
let real_k = cmp::min(n - k, k); let real_k = cmp::min(n - k, k);
if real_k == 0 { if real_k == 0 {
return Some(1); return Some(1);
} }
let mut result: u64 = 1; let mut result: u64 = 1;
for i in 0..real_k { for i in 0..real_k {
result = result.checked_mul(n - i).and_then(|r| r.checked_div(i + 1))?; result = result.checked_mul(n - i)?.checked_div(i + 1)?;
} }
Some(result) i64::try_from(result).ok()
} }
/// Round a number down to the nearest integer. /// Round a number down to the nearest integer.

View File

@ -1,6 +1,6 @@
pub use typst_macros::{cast_from_value, cast_to_value, Cast}; pub use typst_macros::{cast_from_value, cast_to_value, Cast};
use std::num::{NonZeroI64, NonZeroUsize}; use std::num::{NonZeroI64, NonZeroU64, NonZeroUsize};
use std::ops::Add; use std::ops::Add;
use ecow::EcoString; use ecow::EcoString;
@ -95,8 +95,19 @@ cast_to_value! {
v: u32 => Value::Int(v as i64) v: u32 => Value::Int(v as i64)
} }
cast_from_value! {
u64,
int: i64 => int.try_into().map_err(|_| {
if int < 0 {
"number must be at least zero"
} else {
"number too large"
}
})?,
}
cast_to_value! { cast_to_value! {
v: i32 => Value::Int(v as i64) v: u64 => Value::Int(v as i64)
} }
cast_from_value! { cast_from_value! {
@ -114,19 +125,38 @@ cast_to_value! {
v: usize => Value::Int(v as i64) v: usize => Value::Int(v as i64)
} }
cast_to_value! {
v: i32 => Value::Int(v as i64)
}
cast_from_value! { cast_from_value! {
u64, NonZeroI64,
int: i64 => int.try_into().map_err(|_| { int: i64 => int.try_into()
if int < 0 { .map_err(|_| if int == 0 {
"number must be at least zero" "number must not be zero"
} else { } else {
"number too large" "number too large"
}
})?, })?,
} }
cast_to_value! { cast_to_value! {
v: u64 => Value::Int(v as i64) v: NonZeroI64 => Value::Int(v.get())
}
cast_from_value! {
NonZeroU64,
int: i64 => int
.try_into()
.and_then(|int: u64| int.try_into())
.map_err(|_| if int <= 0 {
"number must be positive"
} else {
"number too large"
})?,
}
cast_to_value! {
v: NonZeroU64 => Value::Int(v.get() as i64)
} }
cast_from_value! { cast_from_value! {
@ -145,20 +175,6 @@ cast_to_value! {
v: NonZeroUsize => Value::Int(v.get() as i64) v: NonZeroUsize => Value::Int(v.get() as i64)
} }
cast_from_value! {
NonZeroI64,
int: i64 => int.try_into()
.map_err(|_| if int <= 0 {
"number must be positive"
} else {
"number too large"
})?,
}
cast_to_value! {
v: NonZeroI64 => Value::Int(v.get())
}
cast_from_value! { cast_from_value! {
char, char,
string: Str => { string: Str => {

View File

@ -95,7 +95,7 @@
#calc.pow(2, calc.pow(2.0, 10000.0)) #calc.pow(2, calc.pow(2.0, 10000.0))
--- ---
// Error: 15-18 the result is not a real number // Error: 10-19 the result is not a real number
#calc.pow(-1, 0.5) #calc.pow(-1, 0.5)
--- ---
@ -107,11 +107,11 @@
#calc.log(-1) #calc.log(-1)
--- ---
// Error: 11-12 base may not be zero, NaN, infinite, or subnormal // Error: 20-21 base may not be zero, NaN, infinite, or subnormal
#calc.log(1, base: 0) #calc.log(1, base: 0)
--- ---
// Error: 11-13 the result is not a real number // Error: 10-24 the result is not a real number
#calc.log(10, base: -1) #calc.log(10, base: -1)
--- ---
@ -120,7 +120,7 @@
#test(calc.fact(5), 120) #test(calc.fact(5), 120)
--- ---
// Error: 12-14 the factorial result is too large // Error: 11-15 the result is too large
#calc.fact(21) #calc.fact(21)
--- ---
@ -131,7 +131,7 @@
#test(calc.perm(5, 6), 0) #test(calc.perm(5, 6), 0)
--- ---
// Error: 12-14 the permutation result is too large // Error: 11-19 the result is too large
#calc.perm(21, 21) #calc.perm(21, 21)
--- ---
@ -175,5 +175,5 @@
#range(4, step: "one") #range(4, step: "one")
--- ---
// Error: 18-19 number must be positive // Error: 18-19 number must not be zero
#range(10, step: 0) #range(10, step: 0)