Number library and integrations

This commit adds a number library which handles fractions, floats,
whole numbers, scientific notation, and special symbolic numbers
all according to the R7RS small specification.

Numeric trait is used to abstract operations across all number types
and a Number enum is used to offer a non-opaque type that stores any
kind of number.

Upon the Number enum is implemented the following traits:
- Add, Div, Sub, Mul
- Pow
- PartialEq
- PartialOrd

Which then offer the following operators to use on the Number enum
instances themselves: + - / * == != < > <= >= and of course x.pow(y).

Additionally, the number package contains parsing logic for each type
of number. FromStr is implemented as part of the Numeric trait, and
then in turn implemented on Number. Additionally Into<String> is
implemented for the Numeric trait and then on the Number enum type
as well.

Test cases have been added for basic cases, but could be expanded.

Additional modifications:
- LexError has a custom display implementation that properly outputs
  formatted errors.
- Sexpr package updated to use new number package

Signed-off-by: Ava Affine <ava@sunnypup.io>
This commit is contained in:
Ava Apples Affine 2025-05-15 12:49:08 -07:00
parent 6554a0639a
commit 41216d3526
6 changed files with 992 additions and 31 deletions

782
mycelium/src/number.rs Normal file
View file

@ -0,0 +1,782 @@
/* Mycelium Scheme
* Copyright (C) 2025 Ava Affine
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use alloc::string::String;
use alloc::format;
use alloc::fmt::Debug;
use core::{cmp::Ordering, f64, ops::{Add, Div, Mul, Sub}, str::FromStr};
use num::{integer::{gcd}, pow::{self, Pow}};
pub const E_INCOMPREHENSIBLE: &str = "could not comprehend number literal";
pub const E_BASE_PARSE_FAIL: &str = "failed to parse explicit base literal";
pub const E_POUND_TRUNCATED: &str = "pound sign implies additional input";
pub const E_UNKNOWN_CONTROL: &str = "unknown character in number literal";
pub const E_EMPTY_INPUT: &str = "empty string cannot be a number";
pub const E_UNKNOWN_SYMBOL: &str = "unknown symbolic number repr";
pub const E_NO_DENOMINATOR: &str = "fraction is missing a denominator";
pub const E_MULTI_DENOMINATOR: &str = "fraction has too many denominators";
pub const E_ZERO_DENOMINATOR: &str = "denominator cannot be zero";
pub const E_NUMERATOR_PARSE_FAIL: &str = "couldnt parse numerator";
pub const E_DENOMINATOR_PARSE_FAIL: &str = "couldnt parse denominator";
pub const E_FLOAT_PARSE_FAIL: &str = "couldnt parse float";
pub const E_SCIENTIFIC_E: &str = "scientific notation implies an 'e'";
pub const E_SCIENTIFIC_MULTI_E: &str = "scientific notation implies only a single 'e'";
pub const E_SCIENTIFIC_OPERAND: &str = "couldnt parse 32 bit float operand";
pub const E_SCIENTIFIC_POWER: &str = "couldnt parse integer power";
trait Numeric: Copy + Clone + Debug + FromStr + Into<String> {
fn is_exact(&self) -> bool;
fn make_inexact(&self) -> Float;
fn make_exact(&self) -> Fraction;
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct ScientificNotation (f32, isize);
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum SymbolicNumber {
Inf,
NegInf,
NaN,
NegNan,
}
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Fraction (isize, isize);
#[derive(Copy, Clone, Debug, PartialEq)]
pub struct Float (f64);
#[derive(Copy, Clone, Debug)]
pub enum Number {
Sci(ScientificNotation),
Fra(Fraction),
Flt(Float),
Sym(SymbolicNumber)
}
impl From<SymbolicNumber> for Number {
fn from(value: SymbolicNumber) -> Self {
Number::Sym(value)
}
}
impl From<ScientificNotation> for Number {
fn from(value: ScientificNotation) -> Self {
Number::Sci(value)
}
}
impl From<Fraction> for Number {
fn from(value: Fraction) -> Self {
Number::Fra(value)
}
}
impl From<Float> for Number {
fn from(value: Float) -> Self {
Number::Flt(value)
}
}
// TODO: both the following impls should be done with a macro
impl Into<String> for Number {
fn into(self) -> String {
match self {
Number::Sci(x) => x.into(),
Number::Fra(x) => x.into(),
Number::Flt(x) => x.into(),
Number::Sym(x) => x.into(),
}
}
}
impl Numeric for Number {
fn is_exact(&self) -> bool {
match self {
Number::Sci(x) => x.is_exact(),
Number::Fra(x) => x.is_exact(),
Number::Flt(x) => x.is_exact(),
Number::Sym(x) => x.is_exact(),
}
}
fn make_exact(&self) -> Fraction {
match self {
Number::Sci(x) => x.make_exact(),
Number::Fra(x) => x.make_exact(),
Number::Flt(x) => x.make_exact(),
Number::Sym(x) => x.make_exact(),
}
}
fn make_inexact(&self) -> Float {
match self {
Number::Sci(x) => x.make_inexact(),
Number::Fra(x) => x.make_inexact(),
Number::Flt(x) => x.make_inexact(),
Number::Sym(x) => x.make_inexact(),
}
}
}
impl From<f64> for Number {
fn from(value: f64) -> Self {
match value {
f64::INFINITY => Number::Sym(SymbolicNumber::Inf),
f64::NEG_INFINITY => Number::Sym(SymbolicNumber::NegInf),
_ if value.is_nan() => Number::Sym(SymbolicNumber::NaN),
_ if value.fract() == 0.0 => Number::Fra(Fraction(value as isize, 1)),
_ => Number::Flt(Float(value))
}
}
}
impl FromStr for Number {
type Err = &'static str;
/* Number forms
* - 1.3
* - 1e100
* - 1.3e100
* - +1.3
* - -2.3
* - #d124
* - #o2535 // base 8
* - #x8A3D // base 16
* - #b1011 // base 2
* - 2/4 // inexact
* - #e1/5 // exact (fraction is as is)
* - #e1e1 // exact 1e1 (= 10)
* - #i1/5 // inexact (collapse fraction to decimal)
* - +inf.0, -inf.0, +nan.0, -nan.0
*/
fn from_str(value: &str) -> Result<Self, Self::Err> {
let maybe_sym = value.parse::<SymbolicNumber>();
if maybe_sym.is_ok() {
return Ok(Number::Sym(maybe_sym.unwrap()))
}
/* Only two things that we need to handle here.
* 1. leading with a +/-
* 2. leading with a #i or a #e
* These are mutually exclusive options.
*
* Once they have been managed or ruled out we can
* just try each number type
*/
let mut force_exact = false;
let mut force_inexact = false;
let mut base = 0;
let mut iter = value.chars();
let mut start_idx: usize = 0;
match iter.next() {
Some('+') => start_idx = 1,
Some('-') => start_idx = 0,
Some('#') => {
start_idx = 2;
match iter.next() {
None => return Err(E_POUND_TRUNCATED),
Some('i') => force_inexact = true,
Some('e') => force_exact = true,
Some('x') => base = 16,
Some('d') => base = 10,
Some('o') => base = 8,
Some('b') => base = 2,
_ => return Err(E_UNKNOWN_CONTROL),
}
},
None => return Err(E_EMPTY_INPUT),
_ => ()
}
let substr = &value[start_idx..];
let res;
if base > 0 {
let num = isize::from_str_radix(substr, base)
.or(Err(E_BASE_PARSE_FAIL))?;
return Ok(Number::Fra(Fraction(num, 1)));
} else if let Ok(num) = substr.parse::<ScientificNotation>() {
res = Number::Sci(num);
} else if let Ok(num) = substr.parse::<Fraction>() {
res = Number::Fra(num);
} else if let Ok(num) = substr.parse::<Float>() {
res = Number::Flt(num);
} else {
return Err(E_INCOMPREHENSIBLE)
}
if force_exact {
return Ok(Number::Fra(res.make_exact()))
}
if force_inexact {
return Ok(Number::Flt(res.make_inexact()))
}
Ok(res)
}
}
impl Add for Number {
type Output = Number;
fn add(self, rhs: Self) -> Self::Output {
if self.is_exact() && rhs.is_exact() {
let Fraction(lnum, lden) = self.make_exact();
let Fraction(rnum, rden) = rhs.make_exact();
let num = (lnum * rden) + (rnum * lden);
let den = lden * rden;
Number::Fra(Fraction(num, den).simplify())
} else {
let Float(l) = self.make_inexact();
let Float(r) = rhs.make_inexact();
let res = l + r;
res.into()
}
}
}
impl Sub for Number {
type Output = Number;
fn sub(self, rhs: Self) -> Self::Output {
if self.is_exact() && rhs.is_exact() {
let Fraction(lnum, lden) = self.make_exact();
let Fraction(rnum, rden) = rhs.make_exact();
let num = (lnum * rden) - (rnum * lden);
let den = lden * rden;
Number::Fra(Fraction(num, den).simplify())
} else {
let Float(l) = self.make_inexact();
let Float(r) = rhs.make_inexact();
let res = l - r;
res.into()
}
}
}
impl Mul for Number {
type Output = Number;
fn mul(self, rhs: Self) -> Self::Output {
if self.is_exact() && rhs.is_exact() {
let Fraction(lnum, lden) = self.make_exact();
let Fraction(rnum, rden) = rhs.make_exact();
let num = lnum * rnum;
let den = lden * rden;
Number::Fra(Fraction(num, den).simplify())
} else {
let Float(l) = self.make_inexact();
let Float(r) = rhs.make_inexact();
let res = l * r;
res.into()
}
}
}
impl Div for Number {
type Output = Number;
fn div(self, rhs: Self) -> Self::Output {
if self.is_exact() && rhs.is_exact() {
let Fraction(lnum, lden) = self.make_exact();
let Fraction(rnum, rden) = rhs.make_exact();
let num = lnum * rden;
let den = rnum * lden;
Number::Fra(Fraction(num, den).simplify())
} else {
let Float(l) = self.make_inexact();
let Float(r) = rhs.make_inexact();
let res = l / r;
res.into()
}
}
}
impl Pow<Number> for Number {
type Output = Number;
fn pow(self, rhs: Number) -> Self::Output {
if self.is_exact() && rhs.is_exact() {
let Fraction(lnum, lden) = self.make_exact();
let Fraction(mut rnum, mut rden) = rhs.make_exact();
// normalize the negative to the top of the fraction
if rden < 0 {
rnum = 0 - rnum;
rden = 0 - rden;
}
// apply whole exponent (numerator)
let mut intermediate_numer =
pow::pow(lnum, rnum.abs() as usize) as f64;
let mut intermediate_denom =
pow::pow(lden, rnum.abs() as usize) as f64;
// handle negative exponent
if rnum < 0 {
intermediate_numer = 1.0 / intermediate_numer;
intermediate_denom = 1.0 / intermediate_denom;
}
// dont bother taking an nth root where n=1
if rden == 1 {
if intermediate_numer.fract() == 0.0 &&
intermediate_denom.fract() == 0.0 {
// return whatever the float decides :)
(intermediate_numer / intermediate_denom).into()
// we still have whole numbers everywhere
} else {
Number::Fra(Fraction(intermediate_numer as isize, intermediate_denom as isize))
}
// gotta take an nth root (right hand denom > 1)
} else {
let num_res =
f64::powf(intermediate_numer as f64, 1.0 / rden as f64);
let den_res =
f64::powf(intermediate_denom as f64, 1.0 / rden as f64);
if num_res.fract() == 0.0 && den_res.fract() == 0.0 {
Number::Fra(Fraction(num_res as isize, den_res as isize))
} else {
(num_res / den_res).into()
}
}
} else {
let Float(l) = self.make_inexact();
let Float(r) = rhs.make_inexact();
let res = f64::powf(l, r);
res.into()
}
}
}
impl PartialEq for Number {
fn eq(&self, other: &Number) -> bool {
//if self.is_exact() && other.is_exact() {
// TODO: Figure out a way to comp two fractions without reducing
// to a float and losing the precision of exact numbers
//} else {
self.make_inexact() == other.make_inexact()
//}
}
}
impl PartialOrd for Number{
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
//if self.is_exact() && other.is_exact() {
// TODO: Figure out a way to comp two fractions without reducing
// to a float and losing the precision of exact numbers
//} else {
self.make_inexact().0.partial_cmp(&other.make_inexact().0)
//}
}
}
impl FromStr for SymbolicNumber {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
match value {
"+inf.0" => Ok(SymbolicNumber::Inf),
"-inf.0" => Ok(SymbolicNumber::NegInf),
"+nan.0" => Ok(SymbolicNumber::NaN),
"-nan.0" => Ok(SymbolicNumber::NegNan),
_ => Err(E_UNKNOWN_SYMBOL)
}
}
}
impl Into<String> for SymbolicNumber {
fn into(self) -> String {
match self {
SymbolicNumber::Inf => format!("+inf.0"),
SymbolicNumber::NegInf => format!("-inf.0"),
SymbolicNumber::NaN => format!("+nan.0"),
SymbolicNumber::NegNan => format!("-nan.0"),
}
}
}
impl Numeric for SymbolicNumber {
fn is_exact(&self) -> bool {
false
}
fn make_inexact(&self) -> Float {
match self {
SymbolicNumber::Inf => Float(f64::INFINITY),
SymbolicNumber::NegInf => Float(f64::NEG_INFINITY),
SymbolicNumber::NaN | SymbolicNumber::NegNan => Float(f64::NAN),
}
}
fn make_exact(&self) -> Fraction {
panic!("attempted to make inf or nan into fraction")
}
}
impl Fraction {
fn simplify(&self) -> Fraction {
let g = gcd(self.0, self.1);
Fraction(self.0 / g, self.1 / g)
}
}
impl FromStr for Fraction {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let part: usize;
if let Some(idx) = value.find('/') {
part = idx;
} else {
return Err(E_NO_DENOMINATOR)
}
// make sure there is ONLY ONE slash
if let Some(idx) = value.rfind('/') && idx != part {
return Err(E_MULTI_DENOMINATOR)
}
let numerator_text = &value[..part];
let denominator_text = &value[part+1..];
let numerator = numerator_text.parse::<isize>()
.or(Err(E_NUMERATOR_PARSE_FAIL))?;
let denominator = denominator_text.parse::<isize>()
.or(Err(E_DENOMINATOR_PARSE_FAIL))?;
if denominator == 0 {
return Err(E_ZERO_DENOMINATOR)
}
Ok(Fraction(numerator, denominator))
}
}
impl Into<String> for Fraction {
fn into(self) -> String {
format!("#e{}/{}", self.0, self.1)
}
}
impl Numeric for Fraction {
fn is_exact(&self) -> bool {
true
}
fn make_inexact(&self) -> Float {
Float(self.0 as f64 / self.1 as f64)
}
fn make_exact(&self) -> Fraction {
*self
}
}
impl FromStr for Float {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
Ok(Float(value.parse::<f64>().or(Err(E_FLOAT_PARSE_FAIL))?))
}
}
impl Into<String> for Float {
fn into(self) -> String {
format!("#i{}", self.0)
}
}
impl Numeric for Float {
fn is_exact(&self) -> bool {
self.0.fract() == 0.0
}
fn make_inexact(&self) -> Float {
*self
}
fn make_exact(&self) -> Fraction {
if self.0.fract() == 0.0 {
Fraction(self.0 as isize, 1)
} else {
unimplemented!("insert rational approximation procedure here")
}
}
}
impl FromStr for ScientificNotation {
type Err = &'static str;
fn from_str(value: &str) -> Result<Self, Self::Err> {
let part: usize;
if let Some(idx) = value.find('e') {
part = idx;
} else {
return Err(E_SCIENTIFIC_E)
}
// make sure there is ONLY ONE slash
if let Some(idx) = value.rfind('e') && idx != part {
return Err(E_SCIENTIFIC_MULTI_E)
}
let operand_text = &value[..part];
let power_text = &value[part+1..];
let operand = operand_text.parse::<f32>()
.or(Err(E_SCIENTIFIC_OPERAND))?;
let power = power_text.parse::<isize>()
.or(Err(E_SCIENTIFIC_POWER))?;
Ok(ScientificNotation(operand, power))
}
}
impl Into<String> for ScientificNotation {
fn into(self) -> String {
format!("#{}e{}", self.0, self.1)
}
}
impl Numeric for ScientificNotation {
fn is_exact(&self) -> bool {
self.0.fract() == 0.0 && self.1 >= 0
}
fn make_inexact(&self) -> Float {
// TODO: This pow function needs to be replaced with one that can handle negative exponents
Float(self.0 as f64 * pow::pow(10, self.1 as usize) as f64)
}
fn make_exact(&self) -> Fraction {
self.make_inexact().make_exact()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_fraction_tests() {
assert_eq!("2/3".parse::<Fraction>(),
Ok(Fraction(2, 3)));
assert_eq!("0/1".parse::<Fraction>(),
Ok(Fraction(0, 1)));
assert_eq!("-1/34".parse::<Fraction>(),
Ok(Fraction(-1, 34)));
assert_eq!("2".parse::<Fraction>(),
Err(E_NO_DENOMINATOR));
assert_eq!("2/2/2".parse::<Fraction>(),
Err(E_MULTI_DENOMINATOR));
assert_eq!("2/0".parse::<Fraction>(),
Err(E_ZERO_DENOMINATOR));
assert_eq!("3.3/3".parse::<Fraction>(),
Err(E_NUMERATOR_PARSE_FAIL));
assert_eq!("2/two".parse::<Fraction>(),
Err(E_DENOMINATOR_PARSE_FAIL));
}
#[test]
fn parse_scientific_notation_tests() {
assert_eq!("2e3".parse::<ScientificNotation>(),
Ok(ScientificNotation(2.0, 3)));
assert_eq!("0e1".parse::<ScientificNotation>(),
Ok(ScientificNotation(0.0, 1)));
assert_eq!("-1e34".parse::<ScientificNotation>(),
Ok(ScientificNotation(-1.0, 34)));
assert_eq!("3.3e3".parse::<ScientificNotation>(),
Ok(ScientificNotation(3.3, 3)));
assert_eq!("2".parse::<ScientificNotation>(),
Err(E_SCIENTIFIC_E));
assert_eq!("2e2e2".parse::<ScientificNotation>(),
Err(E_SCIENTIFIC_MULTI_E));
assert_eq!("2etwo".parse::<ScientificNotation>(),
Err(E_SCIENTIFIC_POWER));
assert_eq!("twoe2".parse::<ScientificNotation>(),
Err(E_SCIENTIFIC_OPERAND));
}
#[test]
fn parse_number_tests() {
assert_eq!("1.3".parse::<Number>(),
Ok(Number::Flt(Float(1.3))));
assert_eq!("1".parse::<Number>(),
Ok(Number::Flt(Float(1 as f64))));
assert_eq!("1.3e3".parse::<Number>(),
Ok(Number::Sci(ScientificNotation(1.3, 3))));
assert_eq!("+1.3".parse::<Number>(),
Ok(Number::Flt(Float(1.3))));
assert_eq!("-1.3".parse::<Number>(),
Ok(Number::Flt(Float(-1.3))));
assert_eq!("#d234".parse::<Number>(),
Ok(Number::Flt(Float(234.0))));
assert_eq!("#o17".parse::<Number>(),
Ok(Number::Fra(Fraction(15, 1))));
assert_eq!("#xAA".parse::<Number>(),
Ok(Number::Fra(Fraction(170, 1))));
assert_eq!("#b101".parse::<Number>(),
Ok(Number::Flt(Float(5.0))));
assert_eq!("2/4".parse::<Number>(),
Ok(Number::Fra(Fraction(2, 4))));
assert_eq!("#e1/5".parse::<Number>(),
Ok(Number::Fra(Fraction(1, 5))));
assert_eq!("#i1/5".parse::<Number>(),
Ok(Number::Flt(Float(0.2))));
assert_eq!("#e1e1".parse::<Number>(),
Ok(Number::Sci(ScientificNotation(1.0, 1))));
assert_eq!("+inf.0".parse::<Number>(),
Ok(Number::Sym(SymbolicNumber::Inf)));
}
#[test]
fn test_number_addition_subtraction_cases() {
let cases = vec![
vec!["1/5", "4/5", "1/1"],
vec!["1/5", "0.8", "1/1"],
vec!["1e1", "2.0", "12/1"],
vec!["1e1", "2/1", "12/1"],
vec!["1e1", "1/2", "10.5"],
];
cases.iter().for_each(|case| {
println!("+ {:#?}", case);
let x = case[0].parse::<Number>().unwrap();
let y = case[1].parse::<Number>().unwrap();
let z = case[2].parse::<Number>().unwrap();
// test some mathematical properties
assert_eq!(x + y, z);
assert_eq!(x + y, y + x);
assert_eq!(z - x, y);
assert_eq!(x + y - x, y);
});
// theres no reason this should adhere to all the other rules
let x = "+inf.0".parse::<Number>().unwrap();
let y = "1e1".parse::<Number>().unwrap();
let z = "+inf.0".parse::<Number>().unwrap();
assert_eq!(x + y, z);
}
#[test]
fn test_number_multiplication_division_cases() {
let cases = vec![
vec!["1/5", "5e0", "1/1"],
vec!["1/5", "5", "1/1"],
vec!["1/5", "2/1", "2/5"],
vec!["4.4", "1/2", "2.2"],
vec!["12.0", "1/2", "6/1"],
vec!["1e1", "2.0", "20/1"],
vec!["1e1", "2/1", "20/1"],
vec!["1e1", "1/2", "5/1"],
];
cases.iter().for_each(|case| {
println!("+ {:#?}", case);
let x = case[0].parse::<Number>().unwrap();
let y = case[1].parse::<Number>().unwrap();
let z = case[2].parse::<Number>().unwrap();
// test some mathematical properties
assert_eq!(x * y, z);
assert_eq!(x * y, y * x);
assert_eq!(z / x, y);
assert_eq!(x * y / x, y);
});
}
#[test]
fn test_number_pow_cases() {
// TODO: add scientific notation cases
let cases = vec![
vec!["2", "2", "4"],
vec!["2/1", "2/1", "4/1"],
vec!["2/1", "2/-1", "1/4"],
vec!["2/1", "2/2", "2/1"],
vec!["2/1", "2.0", "4/1"]
];
cases.iter().for_each(|case| {
println!("+ {:#?}", case);
let x = case[0].parse::<Number>().unwrap();
let y = case[1].parse::<Number>().unwrap();
let z = case[2].parse::<Number>().unwrap();
assert_eq!(x.pow(y), z);
});
}
#[test]
fn test_number_ord_cases() {
// TODO: add more cases
let cases = vec![
vec!["1/2", "1.0", "1e1"],
];
cases.iter().for_each(|case| {
println!("+ {:#?}", case);
let x = case[0].parse::<Number>().unwrap();
let y = case[1].parse::<Number>().unwrap();
let z = case[2].parse::<Number>().unwrap();
assert!(x < y);
assert!(y < z);
assert!(x < z);
});
}
}