diff --git a/CHANGELOG.md b/CHANGELOG.md index be6b370..e7990f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## [0.5.0] - UNRELEASED + +Feat [#5](https://github.com/coriolinus/calc/issues/5): choose backing value type at evaluation time, not CLI + ## [0.4.0] - 2022-08-07 Fix #14: operations with the `@` operator were being applied twice. diff --git a/Cargo.lock b/Cargo.lock index 060cd2b..00d92c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -110,16 +110,19 @@ checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" [[package]] name = "calc" -version = "0.4.1" +version = "0.5.0" dependencies = [ "anyhow", "clap", + "derive_more", "lalrpop", "lalrpop-util", "lazy_static", "num-runtime-fmt", + "num-traits", "regex", "rustyline", + "strum", "thiserror", ] @@ -192,6 +195,27 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" +[[package]] +name = "derive_more" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "unicode-xid", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -311,9 +335,9 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.7.0" +version = "2.11.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" +checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5" dependencies = [ "equivalent", "hashbrown", @@ -471,6 +495,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "once_cell" version = "1.20.2" @@ -702,6 +735,27 @@ version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" +[[package]] +name = "strum" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af23d6f6c1a224baef9d3f61e287d2761385a5b88fdab4eb4c6f11aeb54c4bcf" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7695ce3845ea4b33927c055a39dc438a45b059f7c1b3d91d38d10355fb8cbca7" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "syn" version = "2.0.90" diff --git a/Cargo.toml b/Cargo.toml index 2e47c4d..c485045 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "calc" -version = "0.4.1" +version = "0.5.0" authors = ["Peter Goodspeed-Niklaus "] edition = "2021" description = "CLI calculator app" @@ -16,11 +16,14 @@ keywords = ["calculator", "cli"] [dependencies] anyhow = { version = "1.0.94", optional = true } clap = { version = "4.5.23", features = ["derive"], optional = true } +derive_more = { version = "2.0.1", features = ["display", "from", "try_into"] } lalrpop-util = { version = "0.20.2", features = ["lexer"] } lazy_static = "1.5.0" num-runtime-fmt = "0.1" +num-traits = "0.2.19" regex = "1.11.1" rustyline = { version = "12.0.0", optional = true } +strum = { version = "0.27.2", features = ["derive"] } thiserror = "1.0.69" [build-dependencies] diff --git a/src/ast.rs b/src/ast.rs index 9d02045..580155a 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -1,10 +1,7 @@ use lalrpop_util::lalrpop_mod; use num_runtime_fmt::NumFmt; -use crate::{ - types::{Calcable, CalcableError}, - Context, -}; +use crate::{Context, Result, Value, ValueError}; // no point getting style warnings for generated code lalrpop_mod!(#[allow(clippy::all)] pub parser); @@ -26,10 +23,10 @@ pub enum PrefixOperator { } impl PrefixOperator { - fn evaluate(&self, operand: N) -> Result::Err> { + fn evaluate(&self, operand: Value) -> Result { match self { - Self::Negation => operand.neg().ok_or_else(|| N::Err::unimplemented("-")), - Self::Not => operand.not().ok_or_else(|| N::Err::unimplemented("!")), + Self::Negation => Ok(-operand), + Self::Not => !operand, } } } @@ -54,26 +51,22 @@ pub enum InfixOperator { } impl InfixOperator { - fn evaluate(&self, left: N, right: N) -> Result::Err> { + fn evaluate(&self, left: Value, right: Value) -> Result { match self { - Self::Add => ::add(left, right), - Self::Sub => ::sub(left, right), - Self::Mul => ::mul(left, right), - Self::Div => ::div(left, right), - Self::TruncDiv => left.trunc_div(right), + Self::Add => Ok(left + right), + Self::Sub => Ok(left - right), + Self::Mul => Ok(left * right), + Self::Div => Ok(left / right), + Self::TruncDiv => Ok(left.trunc_div(right)), + Self::Rem => Ok(left % right), Self::Pow => left.pow(right), - Self::Rem => left.rem(right), - Self::Lshift => left.shl(right), - Self::Rshift => left.shr(right), + Self::Lshift => left << right, + Self::Rshift => left >> right, Self::RotateL => left.rotate_left(right), Self::RotateR => left.rotate_right(right), - Self::BitAnd => left - .bit_and(right) - .ok_or_else(|| N::Err::unimplemented("&")), - Self::BitOr => left.bit_or(right).ok_or_else(|| N::Err::unimplemented("|")), - Self::BitXor => left - .bit_xor(right) - .ok_or_else(|| N::Err::unimplemented("^")), + Self::BitAnd => left & right, + Self::BitOr => left | right, + Self::BitXor => left ^ right, } } } @@ -108,34 +101,33 @@ pub enum Function { } impl Function { - fn evaluate(&self, operand: N) -> Result::Err> { - let (result, symbol) = match self { - Self::Abs => (operand.abs(), "abs"), - Self::Ceil => (operand.ceil(), "ceil"), - Self::Floor => (operand.floor(), "floor"), - Self::Round => (operand.round(), "round"), - Self::Sin => (operand.sin(), "sin"), - Self::Cos => (operand.cos(), "cos"), - Self::Tan => (operand.tan(), "tan"), - Self::Sinh => (operand.sinh(), "sinh"), - Self::Cosh => (operand.cosh(), "cosh"), - Self::Tanh => (operand.tanh(), "tanh"), - Self::Asin => (operand.asin(), "asin"), - Self::Acos => (operand.acos(), "acos"), - Self::Atan => (operand.atan(), "atan"), - Self::Asinh => (operand.asinh(), "asinh"), - Self::Acosh => (operand.acosh(), "acosh"), - Self::Atanh => (operand.atanh(), "atanh"), - Self::Rad => (operand.rad(), "rad"), - Self::Deg => (operand.deg(), "deg"), - Self::Sqrt => (operand.sqrt(), "sqrt"), - Self::Cbrt => (operand.cbrt(), "cbrt"), - Self::Log => (operand.log(), "log"), - Self::Lg => (operand.lg(), "lg"), - Self::Ln => (operand.ln(), "ln"), - Self::Exp => (operand.exp(), "exp"), - }; - result.ok_or_else(|| N::Err::unimplemented(symbol)) + fn evaluate(&self, operand: Value) -> Value { + match self { + Self::Abs => operand.abs(), + Self::Ceil => operand.ceil(), + Self::Floor => operand.floor(), + Self::Round => operand.round(), + Self::Sin => operand.sin(), + Self::Cos => operand.cos(), + Self::Tan => operand.tan(), + Self::Sinh => operand.sinh(), + Self::Cosh => operand.cosh(), + Self::Tanh => operand.tanh(), + Self::Asin => operand.asin(), + Self::Acos => operand.acos(), + Self::Atan => operand.atan(), + Self::Asinh => operand.asinh(), + Self::Acosh => operand.acosh(), + Self::Atanh => operand.atanh(), + Self::Rad => operand.rad(), + Self::Deg => operand.deg(), + Self::Sqrt => operand.sqrt(), + Self::Cbrt => operand.cbrt(), + Self::Log => operand.log(), + Self::Lg => operand.lg(), + Self::Ln => operand.ln(), + Self::Exp => operand.exp(), + } } } @@ -168,31 +160,23 @@ pub enum Term<'input> { } impl<'input> Term<'input> { - fn evaluate(&self, ctx: &Context) -> Result::Err> { + fn evaluate(&self, ctx: &Context) -> Result { match self { - Self::Literal(s) => N::parse_decimal(s), - Self::HexLiteral(s) => N::parse_hex(s), - Self::OctLiteral(s) => N::parse_octal(s), - Self::BinLiteral(s) => N::parse_binary(s), - Self::Constant(Constant::E) => N::E.ok_or_else(|| N::Err::unimplemented("e")), - Self::Constant(Constant::Pi) => N::PI.ok_or_else(|| N::Err::unimplemented("pi")), + Self::Literal(s) => Value::parse_decimal(s).map_err(Into::into), + Self::HexLiteral(s) => Value::parse_hex(s).map_err(Into::into), + Self::OctLiteral(s) => Value::parse_octal(s).map_err(Into::into), + Self::BinLiteral(s) => Value::parse_binary(s).map_err(Into::into), + Self::Constant(Constant::E) => Ok(Value::E), + Self::Constant(Constant::Pi) => Ok(Value::PI), Self::History(kind, idx) => { + let err = || ValueError::HistoryOOB(*kind, *idx, ctx.history.len()); let real_idx = match kind { HistoryIndexKind::Absolute => *idx, HistoryIndexKind::Relative => { - ctx.history.len().checked_sub(*idx).ok_or_else(|| { - N::Err::history_out_of_bounds(*kind, *idx, ctx.history.len()) - })? + ctx.history.len().checked_sub(*idx).ok_or_else(err)? } }; - match ctx.history.get(real_idx) { - Some(n) => Ok(n.clone()), - None => Err(N::Err::history_out_of_bounds( - *kind, - *idx, - ctx.history.len(), - )), - } + ctx.history.get(real_idx).cloned().ok_or_else(err) } } } @@ -210,17 +194,14 @@ pub enum Expr<'input> { impl<'input> Expr<'input> { /// Evaluate this expression into its mathematical result. - pub(crate) fn evaluate( - &self, - ctx: &Context, - ) -> Result::Err> { + pub(crate) fn evaluate(&self, ctx: &Context) -> Result { match self { Self::Term(term) => term.evaluate(ctx), Self::Prefix(prefix, expr) => prefix.evaluate(expr.evaluate(ctx)?), Self::Infix(left, infix, right) => { infix.evaluate(left.evaluate(ctx)?, right.evaluate(ctx)?) } - Self::Func(func, expr) => func.evaluate(expr.evaluate(ctx)?), + Self::Func(func, expr) => Ok(func.evaluate(expr.evaluate(ctx)?)), Self::Group(expr) => expr.evaluate(ctx), } } @@ -228,13 +209,9 @@ impl<'input> Expr<'input> { /// Error produced by [`AnnotatedExpr`]. #[derive(Debug, thiserror::Error)] -pub enum AnnotatedError -where - N: std::fmt::Debug + Calcable, - ::Err: 'static, -{ +pub enum AnnotatedError { #[error(transparent)] - Calculation(::Err), + Calculation(ValueError), #[error("failed to render calculation result in desired format")] Format(#[from] num_runtime_fmt::Error), } @@ -250,16 +227,12 @@ impl<'input> AnnotatedExpr<'input> { /// /// Return the result as a bare type and also formatted according to the /// requested format string. - pub fn evaluate(&self, ctx: &Context) -> Result<(N, String), AnnotatedError> - where - N: std::fmt::Debug + Calcable + num_runtime_fmt::Numeric, - ::Err: 'static, - { + pub fn evaluate(&self, ctx: &Context) -> Result<(Value, String), AnnotatedError> { let value = self .expr .evaluate(ctx) .map_err(AnnotatedError::Calculation)?; - let formatted = self.format.fmt(value.clone())?; + let formatted = self.format.fmt(value)?; Ok((value, formatted)) } } diff --git a/src/lib.rs b/src/lib.rs index 805b3a6..6d002b9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -4,8 +4,6 @@ //! //! - Create a [`Context`]: a reusable type which contains expression history. //! -//! This type is parametrized by the numeric type which all calculations will use. -//! //! - Parse an [`ast::Expr`] with [`ast::parser::ExprParser`]. //! - Evaluate that expression with [`Context::evaluate`]. //! @@ -14,45 +12,37 @@ //! To enable calculation based on your custom numeric type, just impl [`types::Calcable`] for your type. pub mod ast; -pub mod types; +mod value; use ast::{ parser::{AnnotatedExprParser, ExprParser}, AnnotatedError, ParseError as UserParseError, }; use lalrpop_util::ParseError; -use num_runtime_fmt::Numeric; -use types::Calcable; +pub(crate) use value::Result; +pub use value::{ArithmeticError, Error as ValueError, ParseValueError, Value}; /// Calculation context. /// /// Stores a history of calculated values, so that the history lookups (`@`) work properly. /// Also reifies the numeric type backing the calculations. #[derive(Default)] -pub struct Context { - pub history: Vec, +pub struct Context { + pub history: Vec, } #[derive(Debug, thiserror::Error)] -pub enum Error -where - N: std::fmt::Debug + Calcable, - ::Err: 'static, -{ +pub enum Error { #[error("Parsing")] Parse(#[from] ParseError), #[error("Evaluating")] - Eval(#[source] ::Err), + Eval(#[from] ValueError), #[error("Formatting")] Format(#[source] num_runtime_fmt::Error), } -impl From> for Error -where - N: std::fmt::Debug + Calcable, - ::Err: 'static, -{ - fn from(err: AnnotatedError) -> Self { +impl From for Error { + fn from(err: AnnotatedError) -> Self { match err { AnnotatedError::Calculation(err) => Self::Eval(err), AnnotatedError::Format(err) => Self::Format(err), @@ -60,35 +50,27 @@ where } } -impl Context -where - N: std::fmt::Debug + Calcable, - ::Err: 'static, -{ +impl Context { /// Evaluate an expression in this context. /// /// This both returns the calculated value and stores a copy in the context's history. - pub fn evaluate(&mut self, expr: &str) -> Result> { + pub fn evaluate(&mut self, expr: &str) -> Result { let parser = ExprParser::new(); let expr = parser.parse(expr).map_err(|err| err.map_token(|_| ""))?; let result = expr.evaluate(self).map_err(Error::Eval)?; - self.history.push(result.clone()); + self.history.push(result); Ok(result) } } -impl Context -where - N: std::fmt::Debug + Calcable + Numeric, - ::Err: 'static, -{ +impl Context { /// Evaluate an annotated expression in this context. /// /// Annotations can include output formatting directives. Therefore, the return value /// is a formatted `String`. /// /// This also stores a copy in the context's history. - pub fn evaluate_annotated(&mut self, expr: &str) -> Result> { + pub fn evaluate_annotated(&mut self, expr: &str) -> Result { let parser = AnnotatedExprParser::new(); let expr = parser.parse(expr).map_err(|err| err.map_token(|_| ""))?; let (result, formatted) = expr.evaluate(self)?; diff --git a/src/main.rs b/src/main.rs index 7143560..4e85bce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,62 +1,21 @@ use anyhow::{bail, Result}; -use calc::{types::Calcable, Context, Error}; +use calc::{Context, Error}; use clap::Parser; -use num_runtime_fmt::Numeric; #[derive(Debug, Parser)] #[structopt(about)] struct Opt { - /// Use 64-bit floating-point values for calculation - #[structopt(short, long)] - f64: bool, - - /// Use unsigned 64-bit integer values for calculation - #[structopt(short, long)] - u64: bool, - - /// Use signed 64-bit integer values for calculation - #[structopt(short, long)] - i64: bool, - /// Expression to evaluate expression: Vec, } impl Opt { - fn get_type(&self) -> Result { - Ok(match (self.f64, self.u64, self.i64) { - (_, false, false) => Type::F64, - (false, true, false) => Type::U64, - (false, false, true) => Type::I64, - _ => bail!("conflicting fundamental type options"), - }) - } - fn expr(&self) -> String { self.expression.join(" ") } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -enum Type { - F64, - U64, - I64, -} - -fn eval_as(ty: Type, expr: &str) -> Result<()> { - match ty { - Type::F64 => eval_and_print::(&mut Context::default(), expr), - Type::U64 => eval_and_print::(&mut Context::default(), expr), - Type::I64 => eval_and_print::(&mut Context::default(), expr), - } -} - -fn eval_and_print(ctx: &mut Context, expr: &str) -> Result<()> -where - N: std::fmt::Debug + Default + Calcable + Numeric, - ::Err: 'static + std::error::Error + Send + Sync, -{ +fn eval_and_print(ctx: &mut Context, expr: &str) -> Result<()> { match ctx.evaluate_annotated(expr) { Ok(n) => println!("{}", n), Err(Error::Eval(err)) => bail!(err), @@ -108,20 +67,8 @@ where Ok(()) } -fn shell_as(ty: Type) -> Result<()> { - match ty { - Type::F64 => shell::(), - Type::U64 => shell::(), - Type::I64 => shell::(), - } -} - -fn shell() -> Result<()> -where - N: std::fmt::Debug + Default + Calcable + Numeric, - ::Err: 'static + std::error::Error + Send + Sync, -{ - let mut ctx = Context::::default(); +fn shell() -> Result<()> { + let mut ctx = Context::default(); let mut rl = rustyline::Editor::<(), _>::new()?; loop { @@ -137,8 +84,8 @@ where fn main() -> Result<()> { let opt = Opt::parse(); if opt.expression.is_empty() { - shell_as(opt.get_type()?) + shell() } else { - eval_as(opt.get_type()?, &opt.expr()) + eval_and_print(&mut Context::default(), &opt.expr()) } } diff --git a/src/types/f64.rs b/src/types/f64.rs deleted file mode 100644 index 7ce3e68..0000000 --- a/src/types/f64.rs +++ /dev/null @@ -1,191 +0,0 @@ -use super::{not_implemented, ArithmeticError, BasicError, Calcable}; - -pub type Error = BasicError; -pub type Result = std::result::Result; - -impl Calcable for f64 { - type Err = Error; - - const E: Option = Some(std::f64::consts::E); - const PI: Option = Some(std::f64::consts::PI); - - fn parse_binary(_s: &str) -> Result { - not_implemented("0b...") - } - - fn parse_octal(_s: &str) -> Result { - not_implemented("0o...") - } - - fn parse_decimal(s: &str) -> Result { - s.parse().map_err(BasicError::Parse) - } - - fn parse_hex(_s: &str) -> Result { - not_implemented("0x...") - } - - fn from_f32(f: f32) -> Option { - Some(f as Self) - } - - fn neg(self) -> Option { - Some(-self) - } - - fn not(self) -> Option { - None - } - - fn add(self, other: Self) -> Result { - Ok(self + other) - } - - fn sub(self, other: Self) -> Result { - Ok(self - other) - } - - fn mul(self, other: Self) -> Result { - Ok(self * other) - } - - fn div(self, other: Self) -> Result { - if other == 0.0 { - Err(ArithmeticError::DivideBy0.into()) - } else { - Ok(self / other) - } - } - - fn trunc_div(self, other: Self) -> Result { - self.div(other).map(|quot| quot.floor()) - } - - fn pow(self, other: Self) -> Result { - Ok(self.powf(other)) - } - - fn rem(self, other: Self) -> Result { - if other == 0.0 { - Err(ArithmeticError::DivideBy0.into()) - } else { - Ok(self % other) - } - } - - fn shl(self, _other: Self) -> Result { - not_implemented("<<") - } - - fn shr(self, _other: Self) -> Result { - not_implemented(">>") - } - - fn rotate_left(self, _other: Self) -> Result { - not_implemented("<<<") - } - - fn rotate_right(self, _other: Self) -> Result { - not_implemented(">>>") - } - - fn bit_and(self, _other: Self) -> Option { - None - } - - fn bit_or(self, _other: Self) -> Option { - None - } - - fn bit_xor(self, _other: Self) -> Option { - None - } - - fn abs(self) -> Option { - Some(self.abs()) - } - - fn ceil(self) -> Option { - Some(self.ceil()) - } - - fn floor(self) -> Option { - Some(self.floor()) - } - - fn round(self) -> Option { - Some(self.round()) - } - - fn sin(self) -> Option { - Some(self.sin()) - } - - fn cos(self) -> Option { - Some(self.cos()) - } - - fn tan(self) -> Option { - Some(self.tan()) - } - - fn sinh(self) -> Option { - Some(self.sinh()) - } - - fn cosh(self) -> Option { - Some(self.cosh()) - } - - fn tanh(self) -> Option { - Some(self.tanh()) - } - - fn asin(self) -> Option { - Some(self.asin()) - } - - fn acos(self) -> Option { - Some(self.acos()) - } - - fn atan(self) -> Option { - Some(self.atan()) - } - - fn asinh(self) -> Option { - Some(self.asinh()) - } - - fn acosh(self) -> Option { - Some(self.acosh()) - } - - fn atanh(self) -> Option { - Some(self.atanh()) - } - - fn sqrt(self) -> Option { - Some(self.sqrt()) - } - - fn cbrt(self) -> Option { - Some(self.cbrt()) - } - - fn log(self) -> Option { - Some(self.log10()) - } - - fn lg(self) -> Option { - Some(self.log2()) - } - - fn ln(self) -> Option { - Some(self.ln()) - } - - fn exp(self) -> Option { - Some(self.exp()) - } -} diff --git a/src/types/i64.rs b/src/types/i64.rs deleted file mode 100644 index e7f2a33..0000000 --- a/src/types/i64.rs +++ /dev/null @@ -1,191 +0,0 @@ -use super::{clean_input, ArithmeticError, BasicError, Calcable}; - -pub type Error = BasicError; -pub type Result = std::result::Result; - -fn as_u32(n: i64) -> std::result::Result { - if n > (u32::MAX as i64) { - Err(Error::Arithmetic(ArithmeticError::Overflow)) - } else if n < 0 { - Err(Error::Arithmetic(ArithmeticError::Underflow)) - } else { - Ok(n as u32) - } -} - -impl Calcable for i64 { - type Err = Error; - - const E: Option = None; - const PI: Option = None; - - fn parse_binary(s: &str) -> Result { - i64::from_str_radix(&clean_input(s, "0b"), 2).map_err(Error::Parse) - } - - fn parse_octal(s: &str) -> Result { - i64::from_str_radix(&clean_input(s, "0o"), 8).map_err(Error::Parse) - } - - fn parse_decimal(s: &str) -> Result { - // While we could just use `parse` here instead of `from_str_radix`, this - // usage preserves the pattern. - #[allow(clippy::from_str_radix_10)] - i64::from_str_radix(&clean_input(s, "0d"), 10).map_err(Error::Parse) - } - - fn parse_hex(s: &str) -> Result { - i64::from_str_radix(&clean_input(s, "0x"), 16).map_err(Error::Parse) - } - - fn from_f32(f: f32) -> Option { - Some(f as Self) - } - - fn neg(self) -> Option { - Some(-self) - } - - fn not(self) -> Option { - Some(!self) - } - - fn add(self, other: Self) -> Result { - self.checked_add(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::Overflow)) - } - - fn sub(self, other: Self) -> Result { - self.checked_sub(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::Underflow)) - } - - fn mul(self, other: Self) -> Result { - self.checked_mul(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::Overflow)) - } - - fn div(self, other: Self) -> Result { - self.checked_div(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::DivideBy0)) - } - - fn trunc_div(self, other: Self) -> Result { - self.div(other) - } - - fn pow(self, other: Self) -> Result { - let other = as_u32(other)?; - self.checked_pow(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::Overflow)) - } - - fn rem(self, other: Self) -> Result { - self.checked_rem(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::DivideBy0)) - } - - fn shl(self, other: Self) -> Result { - Ok(self << other) - } - - fn shr(self, other: Self) -> Result { - Ok(self >> other) - } - - fn rotate_left(self, other: Self) -> Result { - Ok(self.rotate_left(as_u32(other)?)) - } - - fn rotate_right(self, other: Self) -> Result { - Ok(self.rotate_right(as_u32(other)?)) - } - - fn bit_and(self, other: Self) -> Option { - Some(self & other) - } - - fn bit_or(self, other: Self) -> Option { - Some(self | other) - } - - fn bit_xor(self, other: Self) -> Option { - Some(self ^ other) - } - - fn abs(self) -> Option { - Some(self.abs()) - } - - fn ceil(self) -> Option { - Some(self) - } - - fn floor(self) -> Option { - Some(self) - } - - fn round(self) -> Option { - Some(self) - } - - fn sin(self) -> Option { - None - } - - fn cos(self) -> Option { - None - } - - fn tan(self) -> Option { - None - } - - fn sinh(self) -> Option { - None - } - - fn cosh(self) -> Option { - None - } - - fn tanh(self) -> Option { - None - } - - fn asin(self) -> Option { - None - } - - fn acos(self) -> Option { - None - } - - fn atan(self) -> Option { - None - } - - fn asinh(self) -> Option { - None - } - - fn acosh(self) -> Option { - None - } - - fn atanh(self) -> Option { - None - } - - fn sqrt(self) -> Option { - None - } - - fn cbrt(self) -> Option { - None - } - - fn ln(self) -> Option { - None - } -} diff --git a/src/types/mod.rs b/src/types/mod.rs deleted file mode 100644 index 1f4a21e..0000000 --- a/src/types/mod.rs +++ /dev/null @@ -1,245 +0,0 @@ -use crate::ast::HistoryIndexKind; -use std::ops::{Add, Div, Mul, Sub}; - -mod f64; -mod i64; -mod u64; - -#[derive(Debug, thiserror::Error)] -pub enum ArithmeticError { - #[error("overflow")] - Overflow, - #[error("underflow")] - Underflow, - #[error("attempt to divide by 0")] - DivideBy0, -} - -#[derive(Debug, thiserror::Error)] -pub enum BasicError { - #[error(transparent)] - Arithmetic(#[from] ArithmeticError), - #[error("operation `{0}` not implemented for {}", std::any::type_name::())] - NotImplemented(&'static str, std::marker::PhantomData), - #[error("parsing: {0}")] - Parse(#[source] E), - #[error("{0:?} history index {1} out of bounds: [0..{2})")] - HistoryOOB(HistoryIndexKind, usize, usize), -} - -pub(crate) fn not_implemented(symbol: &'static str) -> Result> -where - T: std::fmt::Debug, - E: std::error::Error, -{ - Err(BasicError::NotImplemented(symbol, std::marker::PhantomData)) -} - -impl CalcableError for BasicError -where - T: std::fmt::Debug, - E: 'static + std::error::Error, -{ - fn unimplemented(operation: &'static str) -> Self { - Self::NotImplemented(operation, std::marker::PhantomData) - } - - fn history_out_of_bounds( - kind: HistoryIndexKind, - requested_index: usize, - history_len: usize, - ) -> Self { - Self::HistoryOOB(kind, requested_index, history_len) - } -} - -/// A `CalcableError` can always have certain variants. -pub trait CalcableError { - fn unimplemented(operation: &'static str) -> Self; - fn history_out_of_bounds( - kind: HistoryIndexKind, - requested_index: usize, - history_len: usize, - ) -> Self; -} - -/// A trait indicating that this type is suitable for usage in this program. -/// -/// Every type used here has to have basic arithmetic operations defined, but the rest of its -/// behaviors may or may not be defined. Attempts to evaluate an operation which returns `None` -/// will result in an "unimplemented" error message bubbling up to the user. -pub trait Calcable: - Clone - + std::fmt::Display - + Add - + Sub - + Mul - + Div -{ - type Err: std::error::Error + CalcableError; - - const E: Option; - const PI: Option; - - /// Parse a binary input without decimals. - /// - /// Should succeed with or without a leading `0b`. - fn parse_binary(s: &str) -> Result::Err>; - - /// Parse an octal input without decimals. - /// - /// Should succeed with or without a leading `0o`. - fn parse_octal(s: &str) -> Result::Err>; - - /// Parse a decimal input which may or may not contain a decimal point. - /// - /// Should succeed with or without a leading `0d`. - fn parse_decimal(s: &str) -> Result::Err>; - - /// Parse an octal input without decimals. - /// - /// Should succeed with or without a leading `0o`. - fn parse_hex(s: &str) -> Result::Err>; - - /// Instantiate an instance of `Self` from an `f32`. - /// - /// This should be possible with minimal loss for most reasonable types. - fn from_f32(f: f32) -> Option; - - /// Negate this value. - fn neg(self) -> Option; - - /// Bitwise not this value. - fn not(self) -> Option; - - /// Add this value and another, returning an error on overflow. - fn add(self, other: Self) -> Result::Err>; - - /// Subtract another value from this, returning an error on underflow. - fn sub(self, other: Self) -> Result::Err>; - - /// Multiply this value and another, returning an error on overflow. - fn mul(self, other: Self) -> Result::Err>; - - /// Divide this value by another, returning an error on divide by zero. - fn div(self, other: Self) -> Result::Err>; - - /// Divide this value by another, flooring the result to the next lowest integer. - fn trunc_div(self, other: Self) -> Result::Err>; - - /// Raise this value by another. - fn pow(self, other: Self) -> Result::Err>; - - /// Compute the arithmetic remainder of this value and another. - fn rem(self, other: Self) -> Result::Err>; - - /// Compute this value left-shifted by `other` bits. - fn shl(self, other: Self) -> Result::Err>; - - /// Compute this value right-shifted by `other` bits. - fn shr(self, other: Self) -> Result::Err>; - - /// Compute this value left-shifted by `other` bits, wrapping the bits around. - fn rotate_left(self, other: Self) -> Result::Err>; - - /// Compute this value right-shifted by `other` bits, wrapping the bits around. - fn rotate_right(self, other: Self) -> Result::Err>; - - /// Compute this value bitwise anded with another. - fn bit_and(self, other: Self) -> Option; - - /// Compute this value bitwise or'd with another. - fn bit_or(self, other: Self) -> Option; - - /// Compute this value bitwise xor'd with another. - fn bit_xor(self, other: Self) -> Option; - - /// Compute the absolute value of this value. - fn abs(self) -> Option; - - /// Compute the smallest integer greater than or equal to self. - fn ceil(self) -> Option; - - /// Compute the greatest integer less than or equal to self. - fn floor(self) -> Option; - - /// Round self to the nearest integer; halfway cases away from 0.0. - fn round(self) -> Option; - - /// Compute the sine of self. - fn sin(self) -> Option; - - /// Compute the cosine of self. - fn cos(self) -> Option; - - /// Compute the tangent of self. - fn tan(self) -> Option; - - /// Compute the hyperbolic sine of self. - fn sinh(self) -> Option; - - /// Compute the hyperbolic cosine of self. - fn cosh(self) -> Option; - - /// Compute the hyperbolic tangent of self. - fn tanh(self) -> Option; - - /// Compute the arcsine of self. - fn asin(self) -> Option; - - /// Compute the arccosine of self. - fn acos(self) -> Option; - - /// Compute the arctangent of self. - fn atan(self) -> Option; - - /// Compute the inverse hyperbolic sine of self. - fn asinh(self) -> Option; - - /// Compute the inverse hyperbolic cosine of self. - fn acosh(self) -> Option; - - /// Compute the inverse hyperbolic tangent of self. - fn atanh(self) -> Option; - - /// Convert self as degrees to radians. - fn rad(self) -> Option { - Some(Self::PI? / Self::from_f32(180.0)? * self) - } - - /// Convert self as radians to degrees. - fn deg(self) -> Option { - Some(Self::from_f32(180.0)? / Self::PI? * self) - } - - /// Determine the square root of self. - fn sqrt(self) -> Option; - - /// Determine the cube root of self. - fn cbrt(self) -> Option; - - /// Determine the base-10 logarithm of self. - fn log(self) -> Option { - Some(self.ln()? / Self::from_f32(10.0)?.ln()?) - } - - /// Determine the base-2 logarithm of self. - fn lg(self) -> Option { - Some(self.ln()? / Self::from_f32(2.0)?.ln()?) - } - - /// Determine the base-`e` (natural) logarithm of self. - fn ln(self) -> Option; - - /// Determine `e**self` - fn exp(self) -> Option { - Self::E?.pow(self).ok() - } -} - -/// Strip underscores and leading bit markers from the input string -pub(crate) fn clean_input(s: &str, leading: &str) -> String { - let mut input = String::with_capacity(s.len()); - input.extend(s.chars().filter(|&c| c != '_')); - input.trim_start_matches(leading).to_string() -} diff --git a/src/types/u64.rs b/src/types/u64.rs deleted file mode 100644 index fa541b6..0000000 --- a/src/types/u64.rs +++ /dev/null @@ -1,189 +0,0 @@ -use super::{clean_input, ArithmeticError, BasicError, Calcable}; - -pub type Error = BasicError; -pub type Result = std::result::Result; - -fn as_u32(n: u64) -> std::result::Result { - if n > (u32::MAX as u64) { - Err(Error::Arithmetic(ArithmeticError::Overflow)) - } else { - Ok(n as u32) - } -} - -impl Calcable for u64 { - type Err = Error; - - const E: Option = None; - const PI: Option = None; - - fn parse_binary(s: &str) -> Result { - u64::from_str_radix(&clean_input(s, "0b"), 2).map_err(Error::Parse) - } - - fn parse_octal(s: &str) -> Result { - u64::from_str_radix(&clean_input(s, "0o"), 8).map_err(Error::Parse) - } - - fn parse_decimal(s: &str) -> Result { - // While we could just use `parse` here instead of `from_str_radix`, this - // usage preserves the pattern. - #[allow(clippy::from_str_radix_10)] - u64::from_str_radix(&clean_input(s, "0d"), 10).map_err(Error::Parse) - } - - fn parse_hex(s: &str) -> Result { - u64::from_str_radix(&clean_input(s, "0x"), 16).map_err(Error::Parse) - } - - fn from_f32(f: f32) -> Option { - Some(f as Self) - } - - fn neg(self) -> Option { - None - } - - fn not(self) -> Option { - Some(!self) - } - - fn add(self, other: Self) -> Result { - self.checked_add(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::Overflow)) - } - - fn sub(self, other: Self) -> Result { - self.checked_sub(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::Underflow)) - } - - fn mul(self, other: Self) -> Result { - self.checked_mul(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::Overflow)) - } - - fn div(self, other: Self) -> Result { - self.checked_div(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::DivideBy0)) - } - - fn trunc_div(self, other: Self) -> Result { - self.div(other) - } - - fn pow(self, other: Self) -> Result { - let other = as_u32(other)?; - self.checked_pow(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::Overflow)) - } - - fn rem(self, other: Self) -> Result { - self.checked_rem(other) - .ok_or(BasicError::Arithmetic(ArithmeticError::DivideBy0)) - } - - fn shl(self, other: Self) -> Result { - Ok(self << other) - } - - fn shr(self, other: Self) -> Result { - Ok(self >> other) - } - - fn rotate_left(self, other: Self) -> Result { - Ok(self.rotate_left(as_u32(other)?)) - } - - fn rotate_right(self, other: Self) -> Result { - Ok(self.rotate_right(as_u32(other)?)) - } - - fn bit_and(self, other: Self) -> Option { - Some(self & other) - } - - fn bit_or(self, other: Self) -> Option { - Some(self | other) - } - - fn bit_xor(self, other: Self) -> Option { - Some(self ^ other) - } - - fn abs(self) -> Option { - Some(self) - } - - fn ceil(self) -> Option { - Some(self) - } - - fn floor(self) -> Option { - Some(self) - } - - fn round(self) -> Option { - Some(self) - } - - fn sin(self) -> Option { - None - } - - fn cos(self) -> Option { - None - } - - fn tan(self) -> Option { - None - } - - fn sinh(self) -> Option { - None - } - - fn cosh(self) -> Option { - None - } - - fn tanh(self) -> Option { - None - } - - fn asin(self) -> Option { - None - } - - fn acos(self) -> Option { - None - } - - fn atan(self) -> Option { - None - } - - fn asinh(self) -> Option { - None - } - - fn acosh(self) -> Option { - None - } - - fn atanh(self) -> Option { - None - } - - fn sqrt(self) -> Option { - None - } - - fn cbrt(self) -> Option { - None - } - - fn ln(self) -> Option { - None - } -} diff --git a/src/value/arithmetic.rs b/src/value/arithmetic.rs new file mode 100644 index 0000000..c259162 --- /dev/null +++ b/src/value/arithmetic.rs @@ -0,0 +1,154 @@ +use std::ops; + +use super::dispatch_operation; +use crate::Value; + +impl ops::AddAssign for Value +where + Rhs: Into, +{ + fn add_assign(&mut self, rhs: Rhs) { + let mut rhs = rhs.into(); + dispatch_operation!(self, rhs, n, |rhs| *n += rhs); + } +} + +impl ops::Add for Value +where + Rhs: Into, +{ + type Output = Value; + fn add(mut self, rhs: Rhs) -> Value { + let mut rhs = rhs.into(); + dispatch_operation!(&mut self, rhs, n, |rhs| { + *n += rhs; + (*n).into() + }) + } +} + +impl ops::SubAssign for Value +where + Rhs: Into, +{ + fn sub_assign(&mut self, rhs: Rhs) { + let mut rhs = rhs.into(); + if rhs > *self { + self.promote_to_signed(); + } + dispatch_operation!(self, rhs, n, |rhs| *n -= rhs); + } +} + +impl ops::Sub for Value +where + Rhs: Into, +{ + type Output = Value; + + fn sub(mut self, rhs: Rhs) -> Self::Output { + let mut rhs = rhs.into(); + if rhs > self { + self.promote_to_signed(); + } + dispatch_operation!(&mut self, rhs, n, |rhs| { + *n -= rhs; + (*n).into() + }) + } +} + +impl ops::MulAssign for Value +where + Rhs: Into, +{ + fn mul_assign(&mut self, rhs: Rhs) { + let mut rhs = rhs.into(); + dispatch_operation!(self, rhs, n, |rhs| *n *= rhs); + } +} + +impl ops::Mul for Value +where + Rhs: Into, +{ + type Output = Value; + + fn mul(mut self, rhs: Rhs) -> Self::Output { + let mut rhs = rhs.into(); + dispatch_operation!(&mut self, rhs, n, |rhs| { + *n *= rhs; + (*n).into() + }) + } +} + +impl ops::DivAssign for Value +where + Rhs: Into, +{ + fn div_assign(&mut self, rhs: Rhs) { + self.promote_to_float(); + let mut rhs = rhs.into(); + rhs.promote_to_float(); + dispatch_operation!(self, rhs, n, |rhs| *n /= rhs); + } +} + +impl ops::Div for Value +where + Rhs: Into, +{ + type Output = Value; + + fn div(mut self, rhs: Rhs) -> Self::Output { + self.promote_to_float(); + let mut rhs = rhs.into(); + rhs.promote_to_float(); + dispatch_operation!(&mut self, rhs, n, |rhs| { + *n /= rhs; + (*n).into() + }) + } +} + +impl ops::RemAssign for Value +where + Rhs: Into, +{ + fn rem_assign(&mut self, rhs: Rhs) { + let mut rhs = rhs.into(); + dispatch_operation!(self, rhs, n, |rhs| *n %= rhs); + } +} + +impl ops::Rem for Value +where + Rhs: Into, +{ + type Output = Value; + + fn rem(mut self, rhs: Rhs) -> Self::Output { + let mut rhs = rhs.into(); + dispatch_operation!(&mut self, rhs, n, |rhs| { + *n %= rhs; + (*n).into() + }) + } +} + +impl ops::Neg for Value { + type Output = Value; + + fn neg(mut self) -> Self::Output { + self.promote_to_signed(); + match self { + Value::UnsignedInt(_) | Value::UnsignedBigInt(_) => { + unreachable!("we have already promoted out of unsigned territory") + } + Value::SignedInt(n) => (-n).into(), + Value::SignedBigInt(n) => (-n).into(), + Value::Float(n) => (-n).into(), + } + } +} diff --git a/src/value/bitwise.rs b/src/value/bitwise.rs new file mode 100644 index 0000000..fb6b27d --- /dev/null +++ b/src/value/bitwise.rs @@ -0,0 +1,113 @@ +use std::ops; + +use super::{dispatch_operation, Error, Result}; +use crate::Value; + +impl Value { + /// Compute this value left-shifted by `other` bits, wrapping the bits around. + pub fn rotate_left(mut self, right: impl Into) -> Result { + let mut right = right.into(); + dispatch_operation!(INTS: &mut self, right, n, |rhs| { + *n <<= rhs; + (*n).into() + }) + } + + /// Compute this value right-shifted by `other` bits, wrapping the bits around. + pub fn rotate_right(mut self, right: impl Into) -> Result { + let mut right = right.into(); + dispatch_operation!(INTS: &mut self, right, n, |rhs| { + *n <<= rhs; + (*n).into() + }) + } +} + +impl ops::Shl for Value +where + Rhs: Into, +{ + type Output = Result; + + fn shl(mut self, rhs: Rhs) -> Self::Output { + let mut rhs = rhs.into(); + dispatch_operation!(INTS: &mut self, rhs, n, |rhs| { + *n <<= rhs; + (*n).into() + }) + } +} + +impl ops::Shr for Value +where + Rhs: Into, +{ + type Output = Result; + + fn shr(mut self, rhs: Rhs) -> Self::Output { + let mut rhs = rhs.into(); + dispatch_operation!(INTS: &mut self, rhs, n, |rhs| { + *n >>= rhs; + (*n).into() + }) + } +} + +impl ops::BitAnd for Value +where + Rhs: Into, +{ + type Output = Result; + + fn bitand(mut self, rhs: Rhs) -> Self::Output { + let mut rhs = rhs.into(); + dispatch_operation!(INTS: &mut self, rhs, n, |rhs| { + *n &= rhs; + (*n).into() + }) + } +} + +impl ops::BitOr for Value +where + Rhs: Into, +{ + type Output = Result; + + fn bitor(mut self, rhs: Rhs) -> Self::Output { + let mut rhs = rhs.into(); + dispatch_operation!(INTS: &mut self, rhs, n, |rhs| { + *n |= rhs; + (*n).into() + }) + } +} + +impl ops::BitXor for Value +where + Rhs: Into, +{ + type Output = Result; + + fn bitxor(mut self, rhs: Rhs) -> Self::Output { + let mut rhs = rhs.into(); + dispatch_operation!(INTS: &mut self, rhs, n, |rhs| { + *n ^= rhs; + (*n).into() + }) + } +} + +impl ops::Not for Value { + type Output = Result; + + fn not(self) -> Self::Output { + match self { + Value::UnsignedInt(n) => Ok((!n).into()), + Value::UnsignedBigInt(n) => Ok((!n).into()), + Value::SignedInt(n) => Ok((!n).into()), + Value::SignedBigInt(n) => Ok((!n).into()), + Value::Float(_) => Err(Error::ImproperlyFloat), + } + } +} diff --git a/src/value/comparison.rs b/src/value/comparison.rs new file mode 100644 index 0000000..b51299e --- /dev/null +++ b/src/value/comparison.rs @@ -0,0 +1,57 @@ +use std::cmp::Ordering; + +use crate::{value::dispatch_operation, Value}; + +impl Ord for Value { + fn cmp(&self, other: &Self) -> Ordering { + let mut left = *self; + let mut right = *other; + left.match_orders(&mut right); + + match (left, right) { + (Value::UnsignedInt(l), Value::UnsignedInt(r)) => l.cmp(&r), + (Value::UnsignedBigInt(l), Value::UnsignedBigInt(r)) => l.cmp(&r), + (Value::SignedInt(l), Value::SignedInt(r)) => l.cmp(&r), + (Value::SignedBigInt(l), Value::SignedBigInt(r)) => l.cmp(&r), + (Value::Float(l), Value::Float(r)) => l.total_cmp(&r), + _ => unreachable!("both sides have equal orders because we did `match_orders`"), + } + } +} + +impl PartialOrd for Value { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl PartialEq for Value { + fn eq(&self, other: &Self) -> bool { + let mut left = *self; + let mut right = *other; + dispatch_operation!(left, right, l, |r| l == r) + } +} + +impl Eq for Value {} + +impl Value { + /// Perform a strict equality comparison: this is equal if the values have equal value and order _without promotion_. + pub fn strict_eq(self, other: Self) -> bool { + match (self, other) { + (Value::UnsignedInt(l), Value::UnsignedInt(r)) => l == r, + (Value::UnsignedBigInt(l), Value::UnsignedBigInt(r)) => l == r, + (Value::SignedInt(l), Value::SignedInt(r)) => l == r, + (Value::SignedBigInt(l), Value::SignedBigInt(r)) => l == r, + (Value::Float(l), Value::Float(r)) => l == r, + _ => false, + } + } + + /// Compute a strict ordering: this orders first by the [Order][super::Order], then by value only if the orders match + pub fn strict_cmp(self, other: Self) -> Ordering { + self.order() + .cmp(&other.order()) + .then_with(|| self.cmp(&other)) + } +} diff --git a/src/value/error.rs b/src/value/error.rs new file mode 100644 index 0000000..f000dd9 --- /dev/null +++ b/src/value/error.rs @@ -0,0 +1,31 @@ +use crate::ast::HistoryIndexKind; + +#[derive(Debug, thiserror::Error)] +pub enum ArithmeticError { + #[error("overflow")] + Overflow, + #[error("underflow")] + Underflow, + #[error("attempt to divide by 0")] + DivideBy0, +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error(transparent)] + Arithmetic(#[from] ArithmeticError), + #[error("parsing: {0}")] + Parse(#[from] ParseValueError), + #[error("{0:?} history index {1} out of bounds: [0..{2})")] + HistoryOOB(HistoryIndexKind, usize, usize), + #[error("attempted to perform an operation which only makes sense for integers, but value is currently a float")] + ImproperlyFloat, +} + +#[derive(Debug, thiserror::Error)] +pub enum ParseValueError { + #[error("\"{0}\" cannot be parsed as Value")] + Simple(String), + #[error("\"{0}\" cannot be parsed as Value given radix {1}")] + Radix(String, u32), +} diff --git a/src/value/format.rs b/src/value/format.rs new file mode 100644 index 0000000..daa1471 --- /dev/null +++ b/src/value/format.rs @@ -0,0 +1,85 @@ +use num_runtime_fmt::Numeric; + +use crate::Value; + +impl Numeric for Value { + type BinIter = Box>; + type OctIter = Box>; + type DecLeftIter = Box>; + type DecRightIter = Box>; + type HexIter = Box>; + + fn binary(&self) -> Option { + match self { + Value::UnsignedInt(n) => n.binary().map(|iter| Box::new(iter) as _), + Value::UnsignedBigInt(n) => n.binary().map(|iter| Box::new(iter) as _), + Value::SignedInt(n) => n.binary().map(|iter| Box::new(iter) as _), + Value::SignedBigInt(n) => n.binary().map(|iter| Box::new(iter) as _), + Value::Float(n) => n.binary().map(|iter| Box::new(iter) as _), + } + } + + fn octal(&self) -> Option { + match self { + Value::UnsignedInt(n) => n.octal().map(|iter| Box::new(iter) as _), + Value::UnsignedBigInt(n) => n.octal().map(|iter| Box::new(iter) as _), + Value::SignedInt(n) => n.octal().map(|iter| Box::new(iter) as _), + Value::SignedBigInt(n) => n.octal().map(|iter| Box::new(iter) as _), + Value::Float(n) => n.octal().map(|iter| Box::new(iter) as _), + } + } + + fn decimal(&self) -> (Self::DecLeftIter, Option) { + match self { + Value::UnsignedInt(n) => { + let (l, r) = n.decimal(); + let l = Box::new(l) as _; + let r = r.map(|iter| Box::new(iter) as _); + (l, r) + } + Value::UnsignedBigInt(n) => { + let (l, r) = n.decimal(); + let l = Box::new(l) as _; + let r = r.map(|iter| Box::new(iter) as _); + (l, r) + } + Value::SignedInt(n) => { + let (l, r) = n.decimal(); + let l = Box::new(l) as _; + let r = r.map(|iter| Box::new(iter) as _); + (l, r) + } + Value::SignedBigInt(n) => { + let (l, r) = n.decimal(); + let l = Box::new(l) as _; + let r = r.map(|iter| Box::new(iter) as _); + (l, r) + } + Value::Float(n) => { + let (l, r) = n.decimal(); + let l = Box::new(l) as _; + let r = r.map(|iter| Box::new(iter) as _); + (l, r) + } + } + } + + fn hex(&self) -> Option { + match self { + Value::UnsignedInt(n) => n.hex().map(|iter| Box::new(iter) as _), + Value::UnsignedBigInt(n) => n.hex().map(|iter| Box::new(iter) as _), + Value::SignedInt(n) => n.hex().map(|iter| Box::new(iter) as _), + Value::SignedBigInt(n) => n.hex().map(|iter| Box::new(iter) as _), + Value::Float(n) => n.hex().map(|iter| Box::new(iter) as _), + } + } + + fn is_negative(&self) -> bool { + match self { + Value::UnsignedInt(_) | Value::UnsignedBigInt(_) => false, + Value::SignedInt(n) => n.is_negative(), + Value::SignedBigInt(n) => n.is_negative(), + Value::Float(n) => n.is_sign_negative(), + } + } +} diff --git a/src/value/mod.rs b/src/value/mod.rs new file mode 100644 index 0000000..07f8f2d --- /dev/null +++ b/src/value/mod.rs @@ -0,0 +1,311 @@ +mod arithmetic; +mod bitwise; +mod comparison; +mod error; +mod format; +mod numeric; +mod parsing; + +use std::{cmp::Ordering, f64}; + +use num_traits::ToPrimitive as _; + +pub use error::{ArithmeticError, Error, ParseValueError}; + +/// Dispatch an operation across the variants of a value. +/// +/// Parameters: +/// +/// - `$lhs`: expression results in either `mut Value` or `&mut Value` +/// - `$rhs`: expression results in `mut Value` +/// - `$n`: this identifier will be assigned with the value and type of the min-matching-value of `$lhs` +/// - `$op`: this should be a closure which captures `$n` and accepts a single parameter `rhs` of the matching type. +/// It should either return a Value or mutate `$n`. +/// +/// ## Alternates +/// +/// `INTS:` prefix dispatches only across the integers. This alternate returns the `Result` typedef, not a bare `Value`. +macro_rules! dispatch_operation { + ($lhs:expr, $rhs:expr, $n:ident, $op:expr) => {{ + $lhs.match_orders(&mut $rhs); + debug_assert_eq!( + $lhs.order(), + $rhs.order(), + "orders must match after match_orders" + ); + + match $lhs { + Value::UnsignedInt(n) => { + let rhs = u64::try_from($rhs).expect("orders must match"); + let $n = n; + $op(rhs) + } + Value::UnsignedBigInt(n) => { + let rhs = u128::try_from($rhs).expect("orders must match"); + let $n = n; + $op(rhs) + } + Value::SignedInt(n) => { + let rhs = i64::try_from($rhs).expect("orders must match"); + let $n = n; + $op(rhs) + } + Value::SignedBigInt(n) => { + let rhs = i128::try_from($rhs).expect("orders must match"); + let $n = n; + $op(rhs) + } + Value::Float(n) => { + let rhs = f64::try_from($rhs).expect("orders must match"); + let $n = n; + $op(rhs) + } + } + }}; + (INTS: $lhs:expr, $rhs:expr, $n:ident, $op:expr) => {{ + $lhs.match_orders(&mut $rhs); + debug_assert_eq!( + $lhs.order(), + $rhs.order(), + "orders must match after match_orders" + ); + + match $lhs { + Value::UnsignedInt(n) => { + let rhs = u64::try_from($rhs).expect("orders must match"); + let $n = n; + Ok($op(rhs)) + } + Value::UnsignedBigInt(n) => { + let rhs = u128::try_from($rhs).expect("orders must match"); + let $n = n; + Ok($op(rhs)) + } + Value::SignedInt(n) => { + let rhs = i64::try_from($rhs).expect("orders must match"); + let $n = n; + Ok($op(rhs)) + } + Value::SignedBigInt(n) => { + let rhs = i128::try_from($rhs).expect("orders must match"); + let $n = n; + Ok($op(rhs)) + } + Value::Float(_) => Err(Error::ImproperlyFloat), + } + }}; +} + +pub(crate) use dispatch_operation; + +/// A numeric value. +/// +/// Every calculation is parsed calculated as a common value type. +/// This type can be concretely represented by one of a number of memory formats. +/// +/// ## Value Orders +/// +/// The **order** of a value is jargon for its in-memory representation. The +/// orders currently available are: +/// +/// 1. `u64` +/// 1. `u128` +/// 1. `i64` +/// 1. `i128` +/// 1. `f64` +/// +/// Note that in general, lower orders have a narrower scope and higher orders +/// have a broader scope. This enables us to promote values to higher compatible orders +/// as necessary. +/// +/// ### Promotion +/// +/// It will sometimes be necessary to promote a value. The next order after a promotion +/// depends on the value in question. It follows these rules: +/// +/// - `u64` values are unconditionally promoted to `u128` as that conversion is infallible +/// - `u128` values are promoted to the next order in sequence which can represent the type, +/// according to whether or not it fits inside the type bounds. +/// +/// I.e. the value `u64::MAX` would be promoted to `i128`, skipping `i64`, as it could +/// not be losslessly converted. +/// The value `i128::MAX + 1` would be promoted to `f64`, _even though this will lose +/// precision_, because `f64` can better approximate that overflow than `i128` could. +/// - `i64` values are unconditionally promoted to `i128` as that conversion is infallible. +/// - `i128` values are unconditionally promoted to `f64` as that type can better approximate +/// very large values. +/// - `f64` values remain `f64`. +/// +/// ## Parsing Rules +/// +/// When parsing a value, each order is checked in sequence. +/// The first value type which parses without error is used. +/// +/// ## Math Rules +/// +/// When computing an expression, for each pair of values, this algorithm is applied: +/// +/// - the lower-order of the pair is promoted +/// - if the two orders are still not equal, the previous step is repeated +/// - once the two orders are equal, math is performed as normal. +/// +/// ## Equality and Comparison +/// +/// Equality and comparison operations are defined on the logical values. +/// This is to say that when testing equality or comparing values, they are promoted until they match, +/// and then the appropriate calculation is performed. +/// +/// For strict equality comparisons, use the [`strict_eq`][Value::strict_eq] method. For strict ordering, +/// use the [`strict_cmp`][Value::strict_cmp] method. +#[derive( + Debug, + Clone, + Copy, + strum::EnumDiscriminants, + derive_more::From, + derive_more::TryInto, + derive_more::Display, + derive_more::Binary, + derive_more::Octal, + derive_more::LowerHex, + derive_more::UpperHex, + derive_more::LowerExp, + derive_more::UpperExp, +)] +#[strum_discriminants(derive(PartialOrd, Ord))] +#[strum_discriminants(name(Order))] +pub enum Value { + UnsignedInt(u64), + UnsignedBigInt(u128), + SignedInt(i64), + SignedBigInt(i128), + #[binary("{_0}")] + #[octal("{_0}")] + #[lower_hex("{_0}")] + #[upper_hex("{_0}")] + Float(f64), +} + +pub(crate) type Result = std::result::Result; + +impl Value { + pub const PI: Self = Self::Float(f64::consts::PI); + pub const E: Self = Self::Float(f64::consts::E); + + /// Get the order of this value + pub(crate) fn order(&self) -> Order { + Order::from(*self) + } + + /// Promote this value according to its value. + /// + /// - `u64` values are unconditionally promoted to `u128` as that conversion is infallible + /// - `u128` values are promoted to the next order in sequence which can represent the type, + /// according to whether or not it fits inside the type bounds. + /// + /// I.e. the value `u64::MAX` would be promoted to `i128`, skipping `i64`, as it could + /// not be losslessly converted. + /// The value `i128::MAX + 1` would be promoted to `f64`, _even though this will lose + /// precision_, because `f64` can better approximate that overflow than `i128` could. + /// - `i64` values are unconditionally promoted to `i128` as that conversion is infallible. + /// - `i128` values are unconditionally promoted to `f64` as that type can better approximate + /// very large values. + /// - `f64` values remain `f64`. + pub(crate) fn promote(&mut self) { + *self = match *self { + Value::UnsignedInt(n) => Self::UnsignedBigInt(n as _), + Value::UnsignedBigInt(n) => { + const SI_MAX: u128 = i64::MAX as _; + const SBI_MIN: u128 = SI_MAX + 1; + const SBI_MAX: u128 = i128::MAX as _; + + match n { + 0..=SI_MAX => Self::SignedInt(n as _), + SBI_MIN..=SBI_MAX => Self::SignedBigInt(n as _), + _ => Self::Float(n.to_f64().expect("all u128 convert to f64")), + } + } + Value::SignedInt(n) => Self::SignedBigInt(n as _), + Value::SignedBigInt(n) => Self::Float(n.to_f64().expect("all i128 convert to f64")), + Value::Float(n) => Self::Float(n), + } + } + + /// Promote this value until it is signed, according to its value. + pub(crate) fn promote_to_signed(&mut self) { + while self.order() <= Order::UnsignedBigInt { + self.promote(); + } + } + + /// Promote this value until it is a float. + pub(crate) fn promote_to_float(&mut self) -> &mut f64 { + // there is no case where an integer value produces NaN when converted to a float + *self = match *self { + Value::UnsignedInt(n) => (n as f64).into(), + Value::UnsignedBigInt(n) => (n as f64).into(), + Value::SignedInt(n) => (n as f64).into(), + Value::SignedBigInt(n) => (n as f64).into(), + Value::Float(n) => n.into(), + }; + let Self::Float(ref mut f) = self else { + unreachable!("we just promoted up to float") + }; + f + } + + /// Demote this value to the narrowest valid container type + pub(crate) fn demote(&mut self) { + const ZERO: f64 = 0.0; + const UI_MAX: f64 = u64::MAX as _; + const UBI_MAX: f64 = u128::MAX as _; + const SI_MIN: f64 = i64::MIN as _; + const SI_MAX: f64 = i64::MAX as _; + const SBI_MIN: f64 = i128::MIN as _; + const SBI_MAX: f64 = i128::MAX as _; + + let value = *self.clone().promote_to_float(); + debug_assert!( + value.fract().abs() < f64::EPSILON, + "we should never demote values not already known to be integral" + ); + + let narrowest_order = [ + (ZERO..=UI_MAX, Order::UnsignedInt), + (ZERO..=UBI_MAX, Order::SignedBigInt), + (SI_MIN..=SI_MAX, Order::SignedInt), + (SBI_MIN..=SBI_MAX, Order::SignedBigInt), + ] + .into_iter() + .find_map(|(range, order)| range.contains(&value).then_some(order)) + .unwrap_or(Order::Float); + + // rhs isn't really necessary, except structurally, for the `dispatch_operation` macro + // maybe it would just vanish under optimization? + let mut rhs = *self; + + *self = dispatch_operation!(self, rhs, n, |_rhs| { + // due to the nature of the macro we're 100% going to perform at least one unnecessary + // cast in every expansion branch of this macro; can't be helped + #[expect(clippy::unnecessary_cast)] + match narrowest_order { + Order::UnsignedInt => (*n as u64).into(), + Order::UnsignedBigInt => (*n as u128).into(), + Order::SignedInt => (*n as i64).into(), + Order::SignedBigInt => (*n as i128).into(), + Order::Float => (*n as f64).into(), + } + }); + } + + /// Find the minimum compatible order for `self` and `other` by promoting the lesser until they match. + pub(crate) fn match_orders(&mut self, other: &mut Self) { + while self.order() != other.order() { + match self.order().cmp(&other.order()) { + Ordering::Equal => unreachable!("orders already known not to be equal"), + Ordering::Less => self.promote(), + Ordering::Greater => other.promote(), + } + } + } +} diff --git a/src/value/numeric.rs b/src/value/numeric.rs new file mode 100644 index 0000000..cfde111 --- /dev/null +++ b/src/value/numeric.rs @@ -0,0 +1,298 @@ +use crate::Value; + +use super::{ArithmeticError, Error, Result}; + +impl Value { + fn as_u32(self) -> Result { + match self { + Value::UnsignedInt(n) => u32::try_from(n).map_err(|_| ArithmeticError::Overflow.into()), + Value::UnsignedBigInt(n) => { + u32::try_from(n).map_err(|_| ArithmeticError::Overflow.into()) + } + Value::SignedInt(n) => u32::try_from(n).map_err(|_| ArithmeticError::Overflow.into()), + Value::SignedBigInt(n) => { + u32::try_from(n).map_err(|_| ArithmeticError::Overflow.into()) + } + Value::Float(n) => { + if n < 0.0 { + return Err(ArithmeticError::Overflow.into()); + } + if n.fract() != 0.0 { + return Err(Error::ImproperlyFloat); + } + // a 64-bit integer has at least enough precision to capture the integer part of this number + let n = n as u64; + + u32::try_from(n).map_err(|_| ArithmeticError::Overflow.into()) + } + } + } + + /// Divide this value by another, flooring the result to the next lowest integer. + pub fn trunc_div(mut self, other: impl Into) -> Self { + self /= other; + if let Value::Float(n) = &mut self { + *n = n.floor(); + } + self.demote(); + self + } + + /// Raise this value by another. + pub fn pow(self, right: impl Into) -> Result { + let right = right.into(); + match self { + Value::UnsignedInt(n) => { + let right = right.as_u32()?; + Ok(n.pow(right).into()) + } + Value::UnsignedBigInt(n) => { + let right = right.as_u32()?; + Ok(n.pow(right).into()) + } + Value::SignedInt(n) => { + let right = right.as_u32()?; + Ok(n.pow(right).into()) + } + Value::SignedBigInt(n) => { + let right = right.as_u32()?; + Ok(n.pow(right).into()) + } + Value::Float(n) => { + if let Value::Float(e) = right { + Ok(n.powf(e).into()) + } else { + let right = right + .as_u32()? + .try_into() + .map_err(|_| ArithmeticError::Overflow)?; + Ok(n.powi(right).into()) + } + } + } + } + + /// Compute the absolute value of this value. + pub fn abs(self) -> Value { + match self { + Value::UnsignedInt(n) => n.into(), + Value::UnsignedBigInt(n) => n.into(), + Value::SignedInt(n) => n.abs().into(), + Value::SignedBigInt(n) => n.abs().into(), + Value::Float(n) => n.abs().into(), + } + } + + /// Compute the smallest integer greater than or equal to self. + pub fn ceil(self) -> Value { + if let Value::Float(n) = self { + let mut out = Value::from(n.ceil()); + out.demote(); + out + } else { + self + } + } + + /// Compute the greatest integer less than or equal to self. + pub fn floor(self) -> Value { + if let Value::Float(n) = self { + let mut out = Value::from(n.floor()); + out.demote(); + out + } else { + self + } + } + + /// Round self to the nearest integer; halfway cases away from 0.0. + pub fn round(self) -> Value { + if let Value::Float(n) = self { + let mut out = Value::from(n.round()); + out.demote(); + out + } else { + self + } + } + + /// Compute the sine of self. + pub fn sin(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.sin(); + } + self + } + + /// Compute the cosine of self. + pub fn cos(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.cos(); + } + self + } + + /// Compute the tangent of self. + pub fn tan(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.tan(); + } + self + } + + /// Compute the hyperbolic sine of self. + pub fn sinh(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.sinh(); + } + self + } + + /// Compute the hyperbolic cosine of self. + pub fn cosh(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.cosh(); + } + self + } + + /// Compute the hyperbolic tangent of self. + pub fn tanh(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.tanh(); + } + self + } + + /// Compute the arcsine of self. + pub fn asin(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.asin(); + } + self + } + + /// Compute the arccosine of self. + pub fn acos(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.acos(); + } + self + } + + /// Compute the arctangent of self. + pub fn atan(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.atan(); + } + self + } + + /// Compute the inverse hyperbolic sine of self. + pub fn asinh(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.asinh(); + } + self + } + + /// Compute the inverse hyperbolic cosine of self. + pub fn acosh(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.acosh(); + } + self + } + + /// Compute the inverse hyperbolic tangent of self. + pub fn atanh(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.atanh(); + } + self + } + + /// Convert self as degrees to radians. + pub fn rad(mut self) -> Value { + { + let f = self.promote_to_float(); + *f *= std::f64::consts::PI / 180.0; + } + self + } + + /// Convert self as radians to degrees. + pub fn deg(mut self) -> Value { + { + let f = self.promote_to_float(); + *f *= 180.0 / std::f64::consts::PI; + } + self + } + + /// Determine the square root of self. + pub fn sqrt(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.sqrt(); + } + self + } + + /// Determine the cube root of self. + pub fn cbrt(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.cbrt(); + } + self + } + + /// Determine the base-10 logarithm of self. + pub fn log(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.log10(); + } + self + } + + /// Determine the base-2 logarithm of self + pub fn lg(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.log2(); + } + self + } + + /// Determine the base-`e` (natural) logarithm of self. + pub fn ln(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.ln(); + } + self + } + + /// Determine `e**self` + pub fn exp(mut self) -> Value { + { + let f = self.promote_to_float(); + *f = f.exp(); + } + self + } +} diff --git a/src/value/parsing.rs b/src/value/parsing.rs new file mode 100644 index 0000000..4746bad --- /dev/null +++ b/src/value/parsing.rs @@ -0,0 +1,71 @@ +use std::str::FromStr; + +use super::Result; +use crate::{ParseValueError, Value}; + +/// Strip underscores and leading bit markers from the input string +fn clean_input(s: &str, leading: &str) -> String { + let mut input = String::with_capacity(s.len()); + input.extend(s.chars().filter(|&c| c != '_')); + input.trim_start_matches(leading).to_owned() +} + +impl Value { + /// Parses an integer from a string slice with digits in a given base. + /// + /// The string is expected to be an optional `+` or `-` sign followed by only digits. + /// Leading and trailing non-digit characters (including whitespace) represent an error. + /// Underscores (which are accepted in Rust literals) also represent an error. + /// + /// This function panics if `radix` is not in `2..=36`. + pub fn from_str_radix(src: &str, radix: u32) -> Result { + u64::from_str_radix(src, radix) + .map(Self::UnsignedInt) + .or_else(|_| u128::from_str_radix(src, radix).map(Self::UnsignedBigInt)) + .or_else(|_| i64::from_str_radix(src, radix).map(Self::SignedInt)) + .or_else(|_| i128::from_str_radix(src, radix).map(Self::SignedBigInt)) + .map_err(|_| ParseValueError::Radix(src.to_owned(), radix)) + } + + /// Parse a binary input without decimals. + /// + /// Should succeed with or without a leading `0b`. + pub fn parse_binary(s: &str) -> Result { + Value::from_str_radix(&clean_input(s, "0b"), 2) + } + + /// Parse an octal input without decimals. + /// + /// Should succeed with or without a leading `0o`. + pub fn parse_octal(s: &str) -> Result { + Value::from_str_radix(&clean_input(s, "0o"), 8) + } + + /// Parse a decimal input which may or may not contain a decimal point. + /// + /// Should succeed with or without a leading `0d`. + pub fn parse_decimal(s: &str) -> Result { + Value::from_str_radix(&clean_input(s, "0d"), 10) + } + + /// Parse an octal input without decimals. + /// + /// Should succeed with or without a leading `0o`. + pub fn parse_hex(s: &str) -> Result { + Value::from_str_radix(&clean_input(s, "0x"), 16) + } +} + +impl FromStr for Value { + type Err = ParseValueError; + + fn from_str(s: &str) -> Result { + s.parse::() + .map(Self::UnsignedInt) + .or_else(|_| s.parse::().map(Self::UnsignedBigInt)) + .or_else(|_| s.parse::().map(Self::SignedInt)) + .or_else(|_| s.parse::().map(Self::SignedBigInt)) + .or_else(|_| s.parse::().map(Self::Float)) + .map_err(|_| ParseValueError::Simple(s.to_owned())) + } +} diff --git a/tests/expression_mode.rs b/tests/expression_mode.rs index c421055..63c968f 100644 --- a/tests/expression_mode.rs +++ b/tests/expression_mode.rs @@ -30,8 +30,8 @@ fn parse_expressions(input: &str) -> Vec { fn assert_expressions(expressions: &[ExpressionCase]) { for ExpressionCase { input, expect } in expressions { - let mut context = Context::::default(); - let result = context.evaluate(&input).unwrap(); + let mut context = Context::default(); + let result = context.evaluate(input).unwrap(); assert_eq!(&result.to_string(), expect); } } diff --git a/tests/shell_mode.rs b/tests/shell_mode.rs index 80d8171..fb9cc67 100644 --- a/tests/shell_mode.rs +++ b/tests/shell_mode.rs @@ -1,4 +1,4 @@ -use calc::{types::Calcable, Context}; +use calc::Context; use lazy_static::lazy_static; use regex::Regex; @@ -39,15 +39,10 @@ fn parse_expressions(input: &str) -> Vec { out } -fn assert_expressions(expressions: &[ShellCase]) -where - N: std::fmt::Debug + Calcable, - ::Err: 'static, - Context: Default, -{ - let mut context = Context::::default(); +fn assert_expressions(expressions: &[ShellCase]) { + let mut context = Context::default(); for ShellCase { input, expect } in expressions { - let result = context.evaluate(&input).unwrap(); + let result = context.evaluate(input).unwrap(); assert_eq!(&result.to_string(), expect); } } @@ -70,7 +65,7 @@ fn readme_2_shell_mode() { "#; let expressions = parse_expressions(CASE); - assert_expressions::(&expressions); + assert_expressions(&expressions); } #[test] @@ -79,11 +74,13 @@ fn issue_14_example_1() { [1]: 528500/100 5285 [2]: @/2 + 2642.5 + [3]: @@//2 2642 "#; let expressions = parse_expressions(CASE); - assert_expressions::(&expressions); + assert_expressions(&expressions); } #[test] @@ -100,5 +97,5 @@ fn issue_14_example_2() { "#; let expressions = parse_expressions(CASE); - assert_expressions::(&expressions); + assert_expressions(&expressions); }