calc_rational

CLI calculator for rational numbers.
git clone https://git.philomathiclife.com/repos/calc_rational
Log | Files | Refs | README

commit 85178007fff28f8df769ae5cd0f509dc160fe95b
parent 39807875e38def044b66ac250be28b7df73d73b1
Author: Zack Newman <zack@philomathiclife.com>
Date:   Mon, 24 Apr 2023 19:27:53 -0600

add rand. add evaluator iterator.

Diffstat:
MCargo.toml | 12+++++++++++-
MREADME.md | 119++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Mlang.tex | 40++++++++++++++++++++++++----------------
Asrc/lending_iterator.rs | 127+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/lib.rs | 841++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----------
Msrc/main.rs | 233++++++++++++++++++++++++++++++++-----------------------------------------------
6 files changed, 1046 insertions(+), 326 deletions(-)

diff --git a/Cargo.toml b/Cargo.toml @@ -9,7 +9,7 @@ license = "MIT OR Apache-2.0" name = "calc_rational" readme = "README.md" repository = "https://git.philomathiclife.com/repos/calc_rational/" -version = "0.2.0" +version = "0.3.0" [lib] name = "calc_lib" @@ -24,6 +24,16 @@ num-bigint = { version = "0.4.3", default-features = false } num-integer = { version = "0.1.45", default-features = false } num-rational = { version = "0.4.1", default-features = false, features = ["num-bigint"] } num-traits = { version = "0.2.15", default-features = false } +rand = { version = "0.8.5", default-features = false, features = ["std", "std_rng"], optional = true } + +[features] +rand = ["dep:rand", "std"] +std = [] +default = ["std"] + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg", "docsrs"] [badges] maintenance = { status = "actively-developed" } diff --git a/README.md b/README.md @@ -38,32 +38,10 @@ round(@, 6) > 2/3 > 0.666666667 -@6 -There are only 4 previous results. -s 1 -Invalid store expression. A store expression must be of the extended regex form: ^[ \t]*s[ \t]*$. -q a -Invalid quit expression. A quit expression must be of the extended regex form: ^[ \t]*q[ \t]*$. -a -Missing terminal expression at position 0. A terminal expression is a decimal literal expression, recall expression, absolute value expression, parenthetical expression, or a round expression. -|1 -Invalid absolute value expression ending at position 2. An absolute value expression is an addition expression enclosed in '||'. -(2 -Invalid parenthetical expression ending at position 2. A parenthetical expression is an addition expression enclosed in '()'. -round(1,10) -Invalid round expression ending at position 9. A round expression is of the form 'round(<add expression>, digit)' -1/(2-2) -Division by zero ending at position 7. -2^(1/2) -Non-integer exponent with a base that was not 0 or 1 ending at position 7. -0^-1 -Non-negative exponent with a base of 0 ending at position 4. -(-1)! -Factorial of a rational number that was not a non-negative integer ending at position 5. -4. -Invalid decimal literal expression ending at position 2. A decimal literal expression must be of the extended regex form: [0-9]+(\.[0-9]+)?. -1+2 a -Trailing symbols starting at position 4. +rand() +> 939435294927814822 +rand(1+9,10!) +> 2660936 q [zack@laptop ~]$ ``` @@ -71,7 +49,7 @@ q ## Expressions The following are the list of expressions in descending order of precedence: - 1. number literals, `@`, `()`, `||`, `round()` + 1. number literals, `@`, `()`, `||`, `round()`, `rand()` 2. `!` 3. `^` 4. `-` (unary negation operator) @@ -111,6 +89,11 @@ With the aforementioned exception of `!`, all spaces and tabs before and after o `round(expression, digit)` rounds `expression` to `digit`-number of fractional digits. An error will be displayed if called incorrectly. +## Rand expression + +`rand(expression, expression)` generates a random 64-integer inclusively between the passed expressions. +An error will be displayed if called incorrectly. `rand()` generates a random 64-bit integer. + ## Numbers A number literal is a non-empty sequence of digits or a non-empty sequence of digits immediately followed by `.` @@ -143,8 +126,8 @@ As emphasized, it does not work on expressions; so both `@@` and `@(1)` are gram ## Character encoding All inputs must only contain the ASCII encoding of the following Unicode scalar values: `0`-`9`, `.`, `+`, `-`, -`*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, &lt;line feed&gt;, &lt;carriage return&gt;, -`q`, and `d`. Any other byte sequences are grammatically incorrect and will lead to an error message. +`*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, &lt;line feed&gt;, &lt;carriage return&gt;, +and `q`. Any other byte sequences are grammatically incorrect and will lead to an error message. ## Errors @@ -173,18 +156,25 @@ target. Note one must be aware of the ASCII encoding requirement. In particular ### Installing ```bash -[zack@laptop ~]$ cargo install calc_rational +[zack@laptop ~]$ cargo install --all-features calc_rational Updating crates.io index - Installing calc_rational v0.2.0 + Installing calc_rational v0.3.0 Compiling autocfg v1.1.0 + Compiling libc v0.2.142 + Compiling cfg-if v1.0.0 + Compiling ppv-lite86 v0.2.17 Compiling num-traits v0.2.15 Compiling num-integer v0.1.45 Compiling num-bigint v0.4.3 Compiling num-rational v0.4.1 - Compiling calc_rational v0.1.2 - Finished release [optimized] target(s) in 6.09s + Compiling getrandom v0.2.9 + Compiling rand_core v0.6.4 + Compiling rand_chacha v0.3.1 + Compiling rand v0.8.5 + Compiling calc_rational v0.3.0 (/home/zack/projects/calc_rational) + Finished release [optimized] target(s) in 5.54s Installing /home/zack/.cargo/bin/calc - Installed package `calc_rational v0.2.0` (executable `calc`) + Installed package `calc_rational v0.3.0` (executable `calc`) ``` ### Building and testing @@ -193,53 +183,70 @@ target. Note one must be aware of the ASCII encoding requirement. In particular [zack@laptop ~]$ git clone https://git.philomathiclife.com/repos/calc_rational Cloning into 'calc_rational'... [zack@laptop ~]$ cd calc_rational/ -[zack@laptop calc_rational]$ cargo build --release +[zack@laptop calc_rational]$ cargo build --release --all-features Updating crates.io index Compiling autocfg v1.1.0 + Compiling libc v0.2.142 + Compiling cfg-if v1.0.0 + Compiling ppv-lite86 v0.2.17 Compiling num-traits v0.2.15 Compiling num-integer v0.1.45 Compiling num-bigint v0.4.3 Compiling num-rational v0.4.1 - Compiling calc_rational v0.2.0 (/home/zack/calc_rational) + Compiling getrandom v0.2.9 + Compiling rand_core v0.6.4 + Compiling rand_chacha v0.3.1 + Compiling rand v0.8.5 + Compiling calc_rational v0.3.0 (/home/zack/calc_rational) Finished release [optimized] target(s) in 8.25s -[zack@laptop calc_rational]$ cargo t +[zack@laptop calc_rational]$ cargo t --release --all-features Compiling autocfg v1.1.0 + Compiling libc v0.2.142 + Compiling cfg-if v1.0.0 + Compiling ppv-lite86 v0.2.17 Compiling num-traits v0.2.15 Compiling num-integer v0.1.45 Compiling num-bigint v0.4.3 Compiling num-rational v0.4.1 - Compiling calc_rational v0.2.0 (/home/zack/calc_rational) - Finished test [unoptimized + debuginfo] target(s) in 2.40s - Running unittests src/lib.rs (target/debug/deps/calc_lib-381715bd94efc206) + Compiling getrandom v0.2.9 + Compiling rand_core v0.6.4 + Compiling rand_chacha v0.3.1 + Compiling rand v0.8.5 + Compiling calc_rational v0.3.0 (/home/zack/projects/calc_rational) + Finished release [optimized] target(s) in 10.35s + Running unittests src/lib.rs (target/release/deps/calc_lib-fa299178069cd874) -running 23 tests +running 26 tests +test cache::tests::test_len ... ok +test tests::abs ... ok +test cache::tests::test_get_unsafe ... ok test cache::tests::test_get ... ok +test cache::tests::test_push ... ok test cache::tests::test_index ... ok -test cache::tests::test_is_empty ... ok -test cache::tests::test_get_unsafe ... ok -test cache::tests::test_len ... ok test cache::tests::test_new ... ok +test cache::tests::test_is_empty ... ok test tests::exit ... ok test cache::tests::test_index_panic - should panic ... ok -test tests::abs ... ok -test cache::tests::test_push ... ok -test tests::factorial ... ok +test tests::mult ... ok +test tests::add ... ok test tests::neg ... ok -test tests::recall_expression ... ok -test tests::par ... ok -test tests::empty ... ok +test tests::eval_iter ... ok +test tests::number_literal ... ok +test tests::rand ... ok +test tests::exp ... ok +test tests::rand_uni ... ignored test tests::eval ... ok +test tests::factorial ... ok +test tests::empty ... ok test tests::store ... ok -test tests::exp ... ok -test tests::add ... ok -test tests::mult ... ok test tests::round ... ok +test tests::recall_expression ... ok +test tests::par ... ok test tests::term ... ok -test tests::number_literal ... ok -test result: ok. 23 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s +test result: ok. 25 passed; 0 failed; 1 ignored; 0 measured; 0 filtered out; finished in 0.00s - Running unittests src/main.rs (target/debug/deps/calc-42b0141dee0be10c) + Running unittests src/main.rs (target/release/deps/calc-c20eab17c4183fda) running 0 tests diff --git a/lang.tex b/lang.tex @@ -6,27 +6,29 @@ We will define the language, \(L\), of our rational number calculator program. Define the set of non-terminal symbols to be \begin{align*} -N = \{&expr, empty, quit, eval, store, add, mult, neg, exp, fact, term,\\ -&r, abs, recall, par, dec, int, ws, digit, prev, l\}. +N = \{&prog, expr, empty, quit, eval, store, add, mult, neg, exp, fact,\\ +&term, rnd, r, abs, recall, par, dec, int, ws, digit, prev, l\}. \end{align*} Define the set of terminal symbols to be \begin{align*} \Sigma = \{&0,1,2,3,4,5,6,7,8,9,.,+,-,*,/,\string^,!,|(,),round,,,@,s,space\\ -&tab,lf,cr,q,d\}. +&rand, tab,lf,cr,q\}. \end{align*} Define the production rules, \(P\), as the following: \begin{enumerate} + \item \(prog \rightarrow expr\ |\ expr\ l\ |\ expr\ l\ prog\) \item \(expr \rightarrow empty\ |\ quit\ |\ eval\ |\ store\) - \item \(empty \rightarrow ws\ l\) - \item \(quit \rightarrow ws\ q\ ws\ l\) - \item \(eval\rightarrow ws\ add\ ws\ l\) - \item \(store\rightarrow ws\ s\ ws\ l\) + \item \(empty \rightarrow ws\) + \item \(quit \rightarrow ws\ q\ ws\) + \item \(eval\rightarrow ws\ add\ ws\) + \item \(store\rightarrow ws\ s\ ws\) \item \(add \rightarrow add\ ws\ +\ ws\ mult\ |\ add\ ws\ -\ ws\ mult\ |\ mult\) \item \(mult \rightarrow mult\ ws\ *\ ws\ neg\ |\ mult\ ws\ /\ ws\ neg\ |\ neg\) \item \(neg \rightarrow -\ ws\ neg\ |\ exp\) \item \(exp \rightarrow fact\ ws\ \string^\ ws\ neg\ |\ fact\) \item \(fact \rightarrow fact!\ |\ term\) \item \(term \rightarrow dec\ |\ par\ |\ recall\ |\ abs\ |\ r\) + \item \(rnd \rightarrow rand(ws)\ |\ rand(ws\ add\ ws,ws\ add\ ws)\) \item \(r \rightarrow round(ws\ add\ ws,ws\ digit\ ws)\) \item \(abs \rightarrow |ws\ add\ ws|\) \item \(recall \rightarrow @\ |\ @prev\) @@ -41,7 +43,7 @@ Define the production rules, \(P\), as the following: Note that the use of spaces above is purely for visualization purposes (e.g., \(digit\ int\) does not actually have a space). Define the start symbol to be \(expr\). Define the unambiguous, context-free grammar to be -\[ G = (N, \Sigma, P, expr). \] +\[ G = (N, \Sigma, P, prog). \] Let \(\mathcal{L}(G)\) be the language generated from \(G\). When \(@\) is not immediately followed by a \(prev\), let it mean \(@1\). \(@prev\) represents the \(prev^{th}\) most-recent result that has been stored from a \(store\) expression. \(lf\) is the Unicode scalar value U+000A, \(cr\) is the Unicode scalar value U+000D, \(space\) is the @@ -49,15 +51,21 @@ Unicode scalar value U+0020, \(tab\) is the Unicode scalar value U+0009, and \(\ define \(\mathbb{Q} \subset L \subset \mathcal{L}(G)\) with \(\mathbb{Q}\) representing the field of rational numbers such that \(L\) extends \(\mathbb{Q}\) with the ability to recall the previous one to eight \(store\) results as well as adds the unary operators \(||\), \(-\), and \(!\) as well as the binary operator \(\string^\) to -mean absolute value, negation, factorial, and exponentiation respectively. It also adds the function \(round\) which -rounds the passed expression to \(digit\)-number of fractional digits. Note that this means for \(mult / exp\), -\(exp\) does not evaluate to 0. Similarly, \(term\string^exp\) is valid iff \(term\) evaluates to 1, \(term\) -evaluates to 0 and \(exp\) evaluates to a non-negative rational number—\(0^{0}\) is defined to be 1—or \(term\) -evaluates to any other rational number and \(exp\) evaluates to an integer. \(!\) is only defined for non-negative -integers. \(@prev\) is only defined iff at least \(prev\) number of previous \(store\) expressions have been -evaluated. From the above grammar, we see the expression precedence in descending order is the following: +mean absolute value, negation, factorial, and exponentiation respectively. Note that this means for \(mult / exp\), +\(exp\) does not evaluate to 0. Similarly, \(term\string^exp\) is valid iff \(term\) evaluates to 1, \(term\) evaluates +to 0 and \(exp\) evaluates to a non-negative rational number—\(0^{0}\) is defined to be 1—or \(term\) evaluates to any +other rational number and \(exp\) evaluates to an integer. \(!\) is only defined for non-negative integers. \(@prev\) is +only defined iff at least \(prev\) number of previous \(store\) expressions have been evaluated. + +It also adds the function \(round\) which rounds the passed expression to \(digit\)-number of fractional digits. +The function \(rand\) when passed no arguments generates a random 64-bit integer. When the function is passed two +arguments, it generates a random 64-bit integer inclusively between the two arguments. In the latter case, the +second argument must evaluate to a number greater than or equal to the first; and there must be at least one 64-bit +integer in that interval. + +From the above grammar, we see the expression precedence in descending order is the following: \begin{enumerate} - \item number literals, \(()\), \(@\), \(||\), \(round()\) + \item number literals, \(()\), \(@\), \(||\), \(round()\), \(rand()\) \item \(!\) \item \(\string^\) \item \(-\) (the unary negation operator) diff --git a/src/lending_iterator.rs b/src/lending_iterator.rs @@ -0,0 +1,127 @@ +#![deny( + unsafe_code, + unused, + warnings, + clippy::all, + clippy::cargo, + clippy::nursery, + clippy::pedantic, + clippy::restriction +)] +#![allow( + clippy::arithmetic_side_effects, + clippy::blanket_clippy_restriction_lints, + clippy::implicit_return, + clippy::integer_arithmetic, + clippy::missing_trait_methods, + clippy::question_mark_used, + clippy::single_char_lifetime_names +)] +/// Generalizes [`Iterator`] by allowing one to yield references. +pub trait LendingIterator { + /// Read [`Iterator::Item`]. + type Item<'a> + where + Self: 'a; + /// Read [`Iterator::next`]. + fn lend_next(&mut self) -> Option<Self::Item<'_>>; + /// Read [`Iterator::size_hint`]. + #[inline] + fn size_hint(&self) -> (usize, Option<usize>) { + (0, None) + } + /// Read [`Iterator::count`]. + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.lend_fold(0, |count, _| count + 1) + } + /// Read [`Iterator::advance_by`]. + + /// # Errors + /// + /// Read [`Iterator::advance_by`]. + #[inline] + fn advance_by(&mut self, n: usize) -> Result<(), usize> { + for i in 0..n { + self.lend_next().ok_or(i)?; + } + Ok(()) + } + /// Read [`Iterator::by_ref`]. + #[inline] + fn by_ref(&mut self) -> &mut Self + where + Self: Sized, + { + self + } + /// Read [`Iterator::try_fold`]. + /// # Errors + /// + /// Read [`Iterator::try_fold`]. + #[inline] + fn lend_try_fold<B, E, F>(&mut self, init: B, mut f: F) -> Result<B, E> + where + Self: Sized, + F: FnMut(B, Self::Item<'_>) -> Result<B, E>, + { + let mut accum = init; + while let Some(x) = self.lend_next() { + accum = f(accum, x)?; + } + Ok(accum) + } + /// Read [`Iterator::fold`]. + #[inline] + fn lend_fold<B, F>(mut self, init: B, mut f: F) -> B + where + Self: Sized, + F: FnMut(B, Self::Item<'_>) -> B, + { + let mut accum = init; + while let Some(x) = self.lend_next() { + accum = f(accum, x); + } + accum + } +} +impl<T> LendingIterator for T +where + T: Iterator, +{ + type Item<'a> = T::Item where Self: 'a; + #[inline] + fn lend_next(&mut self) -> Option<Self::Item<'_>> { + self.next() + } + #[inline] + fn size_hint(&self) -> (usize, Option<usize>) { + self.size_hint() + } + #[inline] + fn count(self) -> usize + where + Self: Sized, + { + self.count() + } + #[inline] + fn advance_by(&mut self, n: usize) -> Result<(), usize> { + for i in 0..n { + self.lend_next().ok_or(i)?; + } + Ok(()) + } + #[inline] + fn by_ref(&mut self) -> &mut Self + where + Self: Sized, + { + self + } + // Due to a bug in GATs, fold and try_fold cannot be + // implemented. +} diff --git a/src/lib.rs b/src/lib.rs @@ -1,7 +1,108 @@ -//! This crate contains logic for an application to parse and evaluate -//! basic rational number arithmetic using standard operator precedence and -//! associativity. +//! # `calc_lib` +//! `calc_lib` is a library for basic rational number arithmetic using standard +//! operator precedence and associativity. Internally, it is +//! based on [`Ratio<T>`](https://docs.rs/num/latest/num/rational/struct.Ratio.html) +//! and [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html). +//! +//! ## Expressions +//! +//! The following are the list of expressions in descending order of precedence: +//! 1. number literals, `@`, `()`, `||`, `round()`, `rand()` +//! 2. `!` +//! 3. `^` +//! 4. `-` (unary negation operator) +//! 5. `*`, `/` +//! 6. `+`, `-` +//! +//! All binary operators are left-associative sans `^` which is right-associative. +//! +//! Any expression is allowed to be enclosed in `()`. Note that parentheses are purely for grouping expressions; +//! in particular, you cannot use them to represent multiplication (e.g., `4(2)` is grammatically incorrect and +//! will result in an error message). +//! +//! Any expression is allowed to be enclosed in `||`. This unary operator represents absolute value. +//! +//! `!` is the factorial operator. Due to its high precedence, something like *-i!^j!* for *i, j ∈ ℕ* is +//! the same thing as *-((i!)^(j!))*. If the expression preceding it does not evaluate to a non-negative integer, +//! then an error will be displayed. Spaces and tabs are *not* ignored; so `1 !` is grammatically incorrect and +//! will result in an error message. +//! +//! `^` is the exponentiation operator. The expression left of the operator can evaluate to any rational number; +//! however the expression right of the operator must evaluate to an integer unless the expression on the left +//! evaluates to `0` or `1`. In the event of the former, the expression right of the operator must evaluate to a +//! non-negative rational number. In the event of the latter, the expression right of the operator can evaluate to +//! any rational number. Note that `0^0` is defined to be 1. +//! +//! The unary operator `-` represents negation. +//! +//! The operators `*` and `/` represent multiplication and division respectively. Expressions right of `/` +//! must evaluate to any non-zero rational number; otherwise an error will be displayed. +//! +//! The binary operators `+` and `-` represent addition and subtraction respectively. +//! +//! With the aforementioned exception of `!`, all spaces and tabs before and after operators are ignored. +//! +//! ## Round expression +//! +//! `round(expression, digit)` rounds `expression` to `digit`-number of fractional digits. An error will +//! be displayed if called incorrectly. +//! +//! ## Rand expression +//! +//! `rand(expression, expression)` generates a random 64-integer inclusively between the passed expressions. +//! An error will be displayed if called incorrectly. `rand()` generates a random 64-bit integer. +//! +//! ## Numbers +//! +//! A number literal is a non-empty sequence of digits or a non-empty sequence of digits immediately followed by `.` +//! which is immediately followed by a non-empty sequence of digits (e.g., `134.901`). This means that number +//! literals represent precisely all possible ratios of non-negative integers to non-negative integers who sole +//! prime factors are 2 or 5. To represent all other rational numbers, the unary operator `-` and binary +//! operator `/` must be used. +//! +//! ## Empty expression +//! +//! The empty expression (i.e., expression that at most only consists of spaces and tabs) will return +//! the result from the previous non-empty and non-store expression in *decimal* form using the minimum number of digits. +//! In the event an infinite number of digits is required, it will be rounded to 9 fractional digits using normal rounding +//! rules first. +//! +//! ## Store expression +//! +//! To store the result of the previous non-empty and non-store expression, one simply passes `s`. In addition to storing the +//! result which will subsequently be available via `@`, it displays the result. At most 8 results can be stored at once; +//! at which point, results that are stored overwrite the oldest result. +//! +//! ## Recall expression +//! +//! `@` is used to recall previously stored results. It can be followed by any *digit* from `1` to `8`. +//! If such a digit does not immediately follow it, then it will be interpreted as if there were a `1`. +//! `@i` returns the *i*-th most-previous stored result where *i ∈ {1, 2, 3, 4, 5, 6, 7, 8}*. +//! Note that spaces and tabs are *not* ignored so `@ 2` is grammatically incorrect and will result in an error message. +//! As emphasized, it does not work on expressions; so both `@@` and `@(1)` are grammatically incorrect. +//! +//! ## Character encoding +//! +//! All inputs must only contain the ASCII encoding of the following Unicode scalar values: `0`-`9`, `.`, `+`, `-`, +//! `*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, &lt;line feed&gt;, &lt;carriage return&gt;, +//! and `q`. Any other byte sequences are grammatically incorrect and will lead to an error message. +//! +//! ## Errors +//! +//! Errors due to a language violation (e.g., dividing by `0`) manifest into an error message. `panic!`s +//! and [`io::Error`](https://doc.rust-lang.org/std/io/struct.Error.html)s caused by writing to the global +//! standard output stream lead to program abortion. +//! +//! ## Exiting +//! +//! `q` with any number of spaces and tabs before and after will cause the program to terminate. +//! +//! ### Formal language specification +//! +//! For a more precise specification of the “calc language”, one can read the +//! [calc language specification](https://git.philomathiclife.com/calc_rational/lang.pdf). #![no_std] +#![cfg_attr(docsrs, feature(doc_cfg))] #![deny( unsafe_code, unused, @@ -19,7 +120,8 @@ clippy::implicit_return, clippy::integer_arithmetic, clippy::into_iter_on_ref, - clippy::question_mark, + clippy::missing_trait_methods, + clippy::question_mark_used, clippy::single_char_lifetime_names, clippy::unseparated_literal_suffix )] @@ -34,10 +136,13 @@ use num_bigint::{BigInt, BigUint, Sign}; use num_integer::Integer; use num_rational::Ratio; use num_traits::Pow; -use E::{ +#[cfg(feature = "rand")] +use num_traits::ToPrimitive; +#[cfg(feature = "rand")] +use rand::{rngs::ThreadRng, RngCore}; +use LangErr::{ DivByZero, ExpDivByZero, ExpIsNotInt, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit, - InvalidRound, InvalidStore, MissingLF, MissingTerm, NotEnoughPrevResults, NotNonNegIntFact, - TrailingSyms, + InvalidRound, InvalidStore, MissingTerm, NotEnoughPrevResults, NotNonNegIntFact, TrailingSyms, }; use O::{Empty, Eval, Exit, Store}; /// Fixed-sized cache that automatically overwrites the oldest data @@ -45,12 +150,12 @@ use O::{Empty, Eval, Exit, Store}; /// [`Cache`] as a very limited but more performant [`VecDeque`][alloc::collections::VecDeque] that only /// adds new data or reads old data. pub mod cache; -/// The error that is returned when attempting -/// to evaluate the input. -#[derive(Clone, Copy, Debug)] -pub enum E { - /// The input was not terminated with a line feed. - MissingLF, +/// Generalizes [`Iterator`] by using +/// generic associated types. +pub mod lending_iterator; +/// Error due to a language violation. +#[derive(Debug)] +pub enum LangErr { /// The input began with a `q` but had non-whitespace /// that followed it. InvalidQuit, @@ -92,12 +197,27 @@ pub enum E { /// The input started with a valid expression but was immediately followed /// by symbols that could not be chained with the preceding expression. TrailingSyms(usize), + /// The input contained an invalid random expression. + #[cfg(feature = "rand")] + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] + InvalidRand(usize), + /// Error when attempting to generate a random number. + #[cfg(feature = "rand")] + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] + RandErr(rand::Error), + /// Error when the second argument is less than first in the rand function. + #[cfg(feature = "rand")] + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] + RandInvalidArgs(usize), + /// Error when there are no 64-bit integers in the interval passed to the random function. + #[cfg(feature = "rand")] + #[cfg_attr(docsrs, doc(cfg(feature = "rand")))] + RandNoInts(usize), } -impl Display for E { +impl Display for LangErr { #[inline] fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match *self { - MissingLF => f.write_str("The last character is not a line feed."), InvalidStore => f.write_str("Invalid store expression. A store expression must be of the extended regex form: ^[ \\t]*s[ \\t]*$."), InvalidQuit => f.write_str("Invalid quit expression. A quit expression must be of the extended regex form: ^[ \\t]*q[ \\t]*$."), DivByZero(u) => write!(f, "Division by zero ending at position {u}."), @@ -109,8 +229,19 @@ impl Display for E { InvalidAbs(u) => write!(f, "Invalid absolute value expression ending at position {u}. An absolute value expression is an addition expression enclosed in '||'."), InvalidPar(u) => write!(f, "Invalid parenthetical expression ending at position {u}. A parenthetical expression is an addition expression enclosed in '()'."), InvalidRound(u) => write!(f, "Invalid round expression ending at position {u}. A round expression is of the form 'round(<add expression>, digit)'"), - MissingTerm(u) => write!(f, "Missing terminal expression at position {u}. A terminal expression is a decimal literal expression, recall expression, absolute value expression, parenthetical expression, or a round expression."), + #[cfg(not(feature = "rand"))] + MissingTerm(u) => write!(f, "Missing terminal expression at position {u}. A terminal expression is a decimal literal expression, recall expression, absolute value expression, parenthetical expression, or round expression."), + #[cfg(feature = "rand")] + MissingTerm(u) => write!(f, "Missing terminal expression at position {u}. A terminal expression is a decimal literal expression, recall expression, absolute value expression, parenthetical expression, round expression, or rand expression."), TrailingSyms(u) => write!(f, "Trailing symbols starting at position {u}."), + #[cfg(feature = "rand")] + Self::InvalidRand(u) => write!(f, "Invalid rand expression ending at position {u}. A rand expression is of the form 'rand()' or 'rand(<add expression>, <add expression>)'."), + #[cfg(feature = "rand")] + Self::RandErr(ref e) => e.fmt(f), + #[cfg(feature = "rand")] + Self::RandInvalidArgs(u) => write!(f, "The second expression passed to the random function evaluated to rational number less than the first ending at position {u}."), + #[cfg(feature = "rand")] + Self::RandNoInts(u) => write!(f, "There are no 64-bit integers within the interval passed to the random function ending at position {u}."), } } } @@ -250,8 +381,8 @@ pub struct Evaluator<'input, 'cache, 'prev, 'exp> { exp_cache: &'exp mut Vec<Ratio<BigInt>>, } impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { - /// Creates an `Evaluator<'input, 'cache, 'prev, 'exp>` based on the supplied arguments. #[inline] + /// Creates an `Evaluator<'input, 'cache, 'prev, 'exp>` based on the supplied arguments. pub fn new( utf8: &'input [u8], cache: &'cache mut Cache<Ratio<BigInt>, 8>, @@ -267,15 +398,17 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { } } /// Evaluates the input consuming the `Evaluator<'input, 'cache, 'exp>`. + /// Requires the input to contain one expression (i.e., if there are + /// multiple newlines, it will error). /// # Errors /// - /// Returns an [`E`] iff the input violates the calc language. + /// Returns a [`LangErr`] iff the input violates the calc language. #[inline] - #[allow(unsafe_code, clippy::indexing_slicing)] - pub fn evaluate(mut self) -> Result<O<'prev>, E> { + #[allow(clippy::indexing_slicing)] + pub fn evaluate(mut self) -> Result<O<'prev>, LangErr> { self.utf8 = if self.utf8.last().map_or(true, |b| *b != b'\n') { - return Err(MissingLF); + self.utf8 } else { &self.utf8[..self.utf8.len() - self @@ -310,11 +443,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { self.get_adds().and_then(move |val| { self.consume_ws(); if self.i == self.utf8.len() { - *self.prev = Some(val); - // SAFETY: - // We know this is not None since we assign the value - // directly above. - Ok(Eval(unsafe { self.prev.as_ref().unwrap_unchecked() })) + Ok(Eval(self.prev.insert(val))) } else { Err(TrailingSyms(self.i)) } @@ -340,7 +469,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { /// This function is used for both addition and subtraction operations which /// themselves are based on multiplication expressions. #[inline] - fn get_adds(&mut self) -> Result<Ratio<BigInt>, E> { + fn get_adds(&mut self) -> Result<Ratio<BigInt>, LangErr> { let mut left = self.get_mults()?; let mut j; self.consume_ws(); @@ -365,7 +494,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { /// This function is used for both multiplication and division operations which /// themselves are based on negation expressions. #[inline] - fn get_mults(&mut self) -> Result<Ratio<BigInt>, E> { + fn get_mults(&mut self) -> Result<Ratio<BigInt>, LangErr> { let mut left = self.get_neg()?; let mut right; let mut j; @@ -394,7 +523,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { /// Evaluates negation expressions as defined in the calc language. /// This function is based on exponentiation expressions. #[inline] - fn get_neg(&mut self) -> Result<Ratio<BigInt>, E> { + fn get_neg(&mut self) -> Result<Ratio<BigInt>, LangErr> { let mut count = 0usize; while let Some(b) = self.utf8.get(self.i) { if *b == b'-' { @@ -412,7 +541,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { /// This function is based on negation expressions. #[inline] #[allow(clippy::unwrap_used, clippy::unwrap_in_result)] - fn get_exps(&mut self) -> Result<Ratio<BigInt>, E> { + fn get_exps(&mut self) -> Result<Ratio<BigInt>, LangErr> { let mut t = self.get_fact()?; let ix = self.exp_cache.len(); let mut prev; @@ -471,7 +600,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { /// Evaluates factorial expressions as defined in the calc language. /// This function is based on terminal expressions. #[inline] - fn get_fact(&mut self) -> Result<Ratio<BigInt>, E> { + fn get_fact(&mut self) -> Result<Ratio<BigInt>, LangErr> { /// Calculates the factorial of `val`. #[inline] fn fact(mut val: BigUint) -> BigUint { @@ -524,7 +653,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { /// recall expressions, absolute value expressions, and round expressions. #[inline] #[allow(clippy::option_if_let_else)] - fn get_term(&mut self) -> Result<Ratio<BigInt>, E> { + fn get_term(&mut self) -> Result<Ratio<BigInt>, LangErr> { match self.get_rational() { Ok(o) => match o { Some(r) => Ok(r), @@ -534,9 +663,20 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { None => match self.get_recall() { Ok(o3) => match o3 { Some(r3) => Ok(r3.clone()), - None => self - .get_abs() - .and_then(|o4| o4.map_or_else(|| self.get_round(), Ok)), + None => match self.get_abs() { + Ok(o4) => match o4 { + Some(r4) => Ok(r4), + #[cfg(feature = "rand")] + None => self + .get_round() + .and_then(|o5| o5.map_or_else(|| self.get_rand(), Ok)), + #[cfg(not(feature = "rand"))] + None => self.get_round().and_then(|o5| { + o5.map_or_else(|| Err(MissingTerm(self.i)), Ok) + }), + }, + Err(e4) => Err(e4), + }, }, Err(e3) => Err(e3), }, @@ -547,18 +687,138 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { Err(e) => Err(e), } } - /// Rounds a value to the specified number of fractional digits. + /// Generates a random 64-bit integer. /// This function is based on addition expressions. /// This is the last terminal expression attempted when needing /// a terminal expression; as a result, it is the only terminal expression /// that does not return an `Option`. + #[cfg(feature = "rand")] #[inline] - fn get_round(&mut self) -> Result<Ratio<BigInt>, E> { + fn get_rand(&mut self) -> Result<Ratio<BigInt>, LangErr> { + /// Generates a random 64-bit integer. + #[inline] + fn rand(rng: &mut ThreadRng) -> Result<i64, rand::Error> { + let mut bytes = [0u8; 8]; + rng.try_fill_bytes(&mut bytes) + .map(|_| i64::from_ne_bytes(bytes)) + } + /// Generates a random 64-bit integer inclusively between the passed arguments. + #[inline] + #[allow( + clippy::as_conversions, + clippy::cast_lossless, + clippy::cast_possible_truncation, + clippy::cast_possible_wrap, + clippy::cast_sign_loss, + clippy::needless_pass_by_value + )] + fn rand_range( + rng: &mut ThreadRng, + lower: Ratio<BigInt>, + upper: Ratio<BigInt>, + i: usize, + ) -> Result<i64, LangErr> { + if lower > upper { + return Err(LangErr::RandInvalidArgs(i)); + } + let lo = lower.ceil(); + let up = upper.floor(); + let lo_int = lo.numer(); + let up_int = up.numer(); + if lo_int > &BigInt::from(i64::MAX) || up_int < &BigInt::from(i64::MIN) { + return Err(LangErr::RandNoInts(i)); + } + let lo_min = lo_int.to_i64().unwrap_or(i64::MIN); + let up_max = up_int.to_i64().unwrap_or(i64::MAX); + if up_max > lo_min || upper.is_integer() || lower.is_integer() { + let low = lo_min as i128; + // range is [1, 2^64]. + let modulus = (up_max as i128 - low + 1) as u128; + // range is [0, i64::MAX]. + // rem represents how many values need to be removed + // when generating a random i64 in order for uniformity. + let rem = (0x0001_0000_0000_0000_0000 % modulus) as u64; + let mut low_adj; + loop { + match rand(rng) { + Ok(v) => { + low_adj = v as u64; + // Since rem is in [0, i64::MAX], + // this is the same as low_adj < 0 + // || low_adj >= rem. + if low_adj >= rem { + return Ok( + // range is [i64::MIN, i64::MAX]; thus casts are safe. + // modulus is up_max - low + 1; so as low grows, + // % shrinks by the same factor. i64::MAX happens + // when low = up_max = i64::MAX or when low = 0, + // up_max = i64::MAX and low_adj is i64::MAX. + ((low_adj as u128 % modulus) as i128 + low) as i64, + ); + } + } + Err(e) => return Err(LangErr::RandErr(e)), + } + } + } else { + Err(LangErr::RandNoInts(i)) + } + } // This is the last kind of terminal expression that is attempted. // If there is no more data, then we have a missing terminal expression. - let Some(b) = self.utf8.get(self.i..self.i.saturating_add(6)) else { + let Some(b) = self.utf8.get(self.i..self.i.saturating_add(5)) else { return Err(MissingTerm(self.i)) }; + if b == b"rand(" { + self.i += 5; + self.consume_ws(); + let i = self.i; + self.utf8.get(self.i).map_or_else( + || Err(LangErr::InvalidRand(i)), + |p| { + if *p == b')' { + self.i += 1; + rand(&mut rand::thread_rng()).map_or_else( + |e| Err(LangErr::RandErr(e)), + |v| Ok(Ratio::from_integer(BigInt::from(v))), + ) + } else { + let add = self.get_adds()?; + let Some(b2) = self.utf8.get(self.i) else { + return Err(LangErr::InvalidRand(self.i)) + }; + if *b2 == b',' { + self.i += 1; + self.consume_ws(); + let add2 = self.get_adds()?; + self.consume_ws(); + let Some(b3) = self.utf8.get(self.i) else { + return Err(LangErr::InvalidRand(self.i)) + }; + if *b3 == b')' { + self.i += 1; + rand_range(&mut rand::thread_rng(), add, add2, self.i) + .map(|v| Ratio::from_integer(BigInt::from(v))) + } else { + Err(LangErr::InvalidRand(self.i)) + } + } else { + Err(LangErr::InvalidRand(self.i)) + } + } + }, + ) + } else { + Err(MissingTerm(self.i)) + } + } + /// Rounds a value to the specified number of fractional digits. + /// This function is based on addition expressions. + #[inline] + fn get_round(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> { + let Some(b) = self.utf8.get(self.i..self.i.saturating_add(6)) else { + return Ok(None) + }; if b == b"round(" { self.i += 6; self.consume_ws(); @@ -589,7 +849,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { self.i += 1; let mult = BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10])).pow(r); - Ok((val * &mult).round() / &mult) + Ok(Some((val * &mult).round() / &mult)) } else { Err(InvalidRound(self.i)) } @@ -599,13 +859,13 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { Err(InvalidRound(self.i)) } } else { - Err(MissingTerm(self.i)) + Ok(None) } } /// Evaluates absolute value expressions as defined in the calc language. /// This function is based on addition expressions. #[inline] - fn get_abs(&mut self) -> Result<Option<Ratio<BigInt>>, E> { + fn get_abs(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> { let Some(b) = self.utf8.get(self.i) else { return Ok(None) }; @@ -635,7 +895,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { /// Evaluates recall expressions as defined in the calc language. #[inline] #[allow(clippy::as_conversions, clippy::cast_possible_truncation)] - fn get_recall(&mut self) -> Result<Option<&Ratio<BigInt>>, E> { + fn get_recall(&mut self) -> Result<Option<&Ratio<BigInt>>, LangErr> { let Some(b) = self.utf8.get(self.i) else { return Ok(None) }; @@ -661,7 +921,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { /// Evaluates parenthetical expressions as defined in the calc language. /// This function is based on addition expressions. #[inline] - fn get_par(&mut self) -> Result<Option<Ratio<BigInt>>, E> { + fn get_par(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> { let Some(b) = self.utf8.get(self.i) else { return Ok(None) }; @@ -691,7 +951,7 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { clippy::cast_lossless, clippy::indexing_slicing )] - fn get_rational(&mut self) -> Result<Option<Ratio<BigInt>>, E> { + fn get_rational(&mut self) -> Result<Option<Ratio<BigInt>>, LangErr> { #[inline] // ControlFlow makes more sense to use in try_fold; however due to a lack // of a map_or_else function, it is easier to simply return a Result with @@ -745,8 +1005,126 @@ impl<'input, 'cache, 'prev, 'exp> Evaluator<'input, 'cache, 'prev, 'exp> { } } } +/// Reads data from `R` passing each line to an [`Evaluator`] to be evaluated. +#[allow(unused)] +pub struct EvalIter<R> { + /// Reader that contains input data. + reader: R, + /// Buffer that is used by `reader` to read + /// data into. + input_buffer: Vec<u8>, + /// Cache of stored results. + cache: Cache<Ratio<BigInt>, 8>, + /// Result of the previous expression. + prev: Option<Ratio<BigInt>>, + /// Buffer used by [`Evaluator`] to process + /// sub-expressions. + exp_buffer: Vec<Ratio<BigInt>>, + /// Random number generator. + #[cfg(feature = "rand")] + rng: ThreadRng, +} +impl<R> EvalIter<R> { + /// Creates a new `EvalIter`. + #[inline] + #[cfg(feature = "rand")] + pub fn new(reader: R) -> Self { + Self { + reader, + input_buffer: Vec::new(), + cache: Cache::new(), + prev: None, + exp_buffer: Vec::new(), + rng: rand::thread_rng(), + } + } + /// Creates a new `EvalIter`. + #[inline] + #[cfg(not(feature = "rand"))] + pub fn new(reader: R) -> Self { + Self { + reader, + input_buffer: Vec::new(), + cache: Cache::new(), + prev: None, + exp_buffer: Vec::new(), + } + } +} +#[cfg(feature = "std")] +extern crate std; +#[cfg(feature = "std")] +use std::io::{BufRead, Error}; +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +/// Error returned from [`EvalIter`] when +/// an expression has an error. +#[derive(Debug)] +pub enum E { + /// Error containing [`Error`] which is returned + /// from [`EvalIter`] when reading from the supplied + /// [`BufRead`]er. + Error(Error), + /// Error containing [`LangErr`] which is returned + /// from [`EvalIter`] when evaluating a single expression. + LangErr(LangErr), +} +#[cfg(feature = "std")] +impl Display for E { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Error(ref e) => e.fmt(f), + Self::LangErr(ref e) => e.fmt(f), + } + } +} +#[cfg(feature = "std")] +use crate::lending_iterator::LendingIterator; +#[cfg(feature = "std")] +#[cfg_attr(docsrs, doc(cfg(feature = "std")))] +impl<R> LendingIterator for EvalIter<R> +where + R: BufRead, +{ + type Item<'a> = Result<O<'a>, E> where Self: 'a; + #[inline] + fn lend_next(&mut self) -> Option<Result<O<'_>, E>> { + self.input_buffer.clear(); + self.exp_buffer.clear(); + self.reader + .read_until(b'\n', &mut self.input_buffer) + .map_or_else( + |e| Some(Err(E::Error(e))), + |c| { + if c == 0 { + None + } else { + Evaluator::new( + self.input_buffer.as_slice(), + &mut self.cache, + &mut self.prev, + &mut self.exp_buffer, + ) + .evaluate() + .map_or_else( + |e| Some(Err(E::LangErr(e))), + |o| match o { + O::Empty(_) | O::Eval(_) | O::Store(_) => Some(Ok(o)), + O::Exit => None, + }, + ) + } + }, + ) + } +} #[cfg(test)] mod tests { + use super::LangErr::{ + DivByZero, ExpDivByZero, ExpIsNotInt, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit, + InvalidRound, MissingTerm, NotEnoughPrevResults, NotNonNegIntFact, TrailingSyms, + }; use super::*; #[test] fn empty() { @@ -851,13 +1229,19 @@ mod tests { == "> -20.3" ); assert!( - match Evaluator::new(b" ", &mut Cache::new(), &mut None, &mut Vec::new()) - .evaluate() - .unwrap_err() - { - E::MissingLF => true, - _ => false, - } + Evaluator::new( + &[0u8; 0], + &mut Cache::new(), + &mut Some(Ratio::new( + BigInt::from_biguint(Sign::Minus, BigUint::new(vec![203])), + BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10])) + )), + &mut Vec::new() + ) + .evaluate() + .unwrap() + .to_string() + == "> -20.3" ); } #[test] @@ -936,7 +1320,7 @@ mod tests { .get_rational() .unwrap_err() { - E::InvalidDec(i) => i == 2, + InvalidDec(i) => i == 2, _ => false, } ); @@ -947,7 +1331,7 @@ mod tests { .get_rational() .unwrap_err() { - E::InvalidDec(i) => i == 2, + InvalidDec(i) => i == 2, _ => false, } ); @@ -1005,7 +1389,7 @@ mod tests { .get_par() .unwrap_err() { - E::InvalidPar(i) => i == 2, + InvalidPar(i) => i == 2, _ => false, } ); @@ -1014,7 +1398,7 @@ mod tests { .get_par() .unwrap_err() { - E::InvalidPar(i) => i == 9, + InvalidPar(i) => i == 9, _ => false, } ); @@ -1081,7 +1465,7 @@ mod tests { .get_recall() .unwrap_err() { - E::NotEnoughPrevResults(count) => count == 0, + NotEnoughPrevResults(count) => count == 0, _ => false, } ); @@ -1091,7 +1475,7 @@ mod tests { .get_recall() .unwrap_err() { - E::NotEnoughPrevResults(count) => count == 0, + NotEnoughPrevResults(count) => count == 0, _ => false, } ); @@ -1102,7 +1486,7 @@ mod tests { .get_recall() .unwrap_err() { - E::NotEnoughPrevResults(count) => count == 0, + NotEnoughPrevResults(count) => count == 0, _ => false, } ); @@ -1176,7 +1560,7 @@ mod tests { .get_recall() .unwrap_err() { - E::NotEnoughPrevResults(count) => count == 1, + NotEnoughPrevResults(count) => count == 1, _ => false, } ); @@ -1207,7 +1591,7 @@ mod tests { .get_recall() .unwrap_err() { - E::NotEnoughPrevResults(count) => count == 2, + NotEnoughPrevResults(count) => count == 2, _ => false, } ); @@ -1273,7 +1657,7 @@ mod tests { .get_abs() .unwrap_err() { - E::InvalidAbs(i) => i == 2, + InvalidAbs(i) => i == 2, _ => false, } ); @@ -1282,7 +1666,7 @@ mod tests { .get_abs() .unwrap_err() { - E::InvalidAbs(i) => i == 8, + InvalidAbs(i) => i == 8, _ => false, } ); @@ -1336,7 +1720,7 @@ mod tests { .get_round() .unwrap_err() { - E::InvalidRound(i) => i == 7, + InvalidRound(i) => i == 7, _ => false, } ); @@ -1345,7 +1729,7 @@ mod tests { .get_round() .unwrap_err() { - E::InvalidRound(i) => i == 8, + InvalidRound(i) => i == 8, _ => false, } ); @@ -1354,7 +1738,7 @@ mod tests { .get_round() .unwrap_err() { - E::InvalidRound(i) => i == 9, + InvalidRound(i) => i == 9, _ => false, } ); @@ -1367,7 +1751,7 @@ mod tests { .get_round() .unwrap_err() { - E::InvalidRound(i) => i == 9, + InvalidRound(i) => i == 9, _ => false, }); assert!( @@ -1375,23 +1759,10 @@ mod tests { .get_round() .unwrap_err() { - E::InvalidRound(i) => i == 8, + InvalidRound(i) => i == 8, _ => false, } ); - // If the input does not start with 'round(', then it's invalid since get_round must only be called as the last terminal expression which means whitespace must be consumed already. - assert!(match Evaluator::new( - b" round(2/3,2)", - &mut Cache::new(), - &mut None, - &mut Vec::new() - ) - .get_round() - .unwrap_err() - { - E::MissingTerm(i) => i == 0, - _ => false, - }); assert!( Evaluator::new( b"round(2, 7)", @@ -1401,7 +1772,10 @@ mod tests { ) .get_round() .unwrap() - == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))) + == Some(Ratio::from_integer(BigInt::from_biguint( + Sign::Plus, + BigUint::new(vec![2]) + ))) ); assert!( Evaluator::new( @@ -1412,11 +1786,141 @@ mod tests { ) .get_round() .unwrap() - == Ratio::new( + == Some(Ratio::new( BigInt::from_biguint(Sign::Plus, BigUint::new(vec![27])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10])) - ) + )) + ); + } + #[test] + #[cfg(feature = "rand")] + fn rand() { + assert!( + match Evaluator::new(b"rand(1", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_rand() + .unwrap_err() + { + LangErr::InvalidRand(i) => i == 6, + _ => false, + } + ); + assert!( + match Evaluator::new(b"rand(1,2", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_rand() + .unwrap_err() + { + LangErr::InvalidRand(i) => i == 8, + _ => false, + } ); + assert!(match Evaluator::new( + b"rand(1/2,3/4)", + &mut Cache::new(), + &mut None, + &mut Vec::new() + ) + .get_rand() + .unwrap_err() + { + LangErr::RandNoInts(i) => i == 13, + _ => false, + }); + assert!(match Evaluator::new( + b"rand(-100000000000000000000000,-1000000000000000000000)", + &mut Cache::new(), + &mut None, + &mut Vec::new() + ) + .get_rand() + .unwrap_err() + { + LangErr::RandNoInts(i) => i == 55, + _ => false, + }); + assert!(match Evaluator::new( + b"rand(2/3,1/3)", + &mut Cache::new(), + &mut None, + &mut Vec::new() + ) + .get_rand() + .unwrap_err() + { + LangErr::RandInvalidArgs(i) => i == 13, + _ => false, + }); + // If the input does not start with 'rand(', then it's invalid since get_rand must only be called as the last terminal expression which means whitespace must be consumed already. + assert!(match Evaluator::new( + b" rand(2/3,2)", + &mut Cache::new(), + &mut None, + &mut Vec::new() + ) + .get_rand() + .unwrap_err() + { + MissingTerm(i) => i == 0, + _ => false, + }); + assert!( + Evaluator::new(b"rand(2, 7)", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_rand() + .map(|r| { + let int = r.numer(); + int >= &BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])) + && *int <= BigInt::from_biguint(Sign::Plus, BigUint::new(vec![7])) + }) + .unwrap() + ); + assert!( + Evaluator::new(b"rand()", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_rand() + .map(|r| { + let int = r.numer(); + int >= &BigInt::from(i64::MIN) && *int <= BigInt::from(i64::MAX) + }) + .unwrap() + ); + for _ in 0..100 { + assert!( + Evaluator::new(b"rand(2, 2)", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_rand() + .map(|r| *r.numer() == BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))) + .unwrap() + ); + } + } + #[cfg(feature = "rand")] + #[test] + #[ignore] + fn rand_uni() { + // Test rand on an interval that is not a power of 2 in size. + // This causes rand to adjust the interval to enforce uniformity. + let mut vals = [0u32; 3]; + let mut vec = Vec::new(); + let mut cache = Cache::new(); + let mut none = None; + const COUNT: u32 = 999999; + for _ in 1..COUNT { + vals[(Evaluator::new(b"rand(-1, 1)", &mut cache, &mut none, &mut vec) + .get_rand() + .unwrap() + .numer() + .to_i32() + .unwrap() + + 1) as usize] += 1; + } + // Test that the distribution is within 1% of what is expected. + assert!(vals + .into_iter() + .try_fold(false, |_, r| { + if r < COUNT * 33 / 100 || r > COUNT * 101 / 300 { + Err(false) + } else { + Ok(true) + } + }) + .unwrap()); } #[test] fn term() { @@ -1449,6 +1953,31 @@ mod tests { BigInt::from_biguint(Sign::Plus, BigUint::new(vec![100])) ) ); + #[cfg(feature = "rand")] + assert!( + Evaluator::new(b"rand()", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_term() + .is_ok() + ); + #[cfg(feature = "rand")] + assert!(Evaluator::new( + b"rand(-13/93, 833)", + &mut Cache::new(), + &mut None, + &mut Vec::new() + ) + .get_term() + .is_ok()); + #[cfg(not(feature = "rand"))] + assert!( + match Evaluator::new(b"rand()", &mut Cache::new(), &mut None, &mut Vec::new()) + .get_term() + .unwrap_err() + { + MissingTerm(i) => i == 0, + _ => false, + } + ); assert!( Evaluator::new(b"|4|", &mut Cache::new(), &mut None, &mut Vec::new()) .get_term() @@ -1461,7 +1990,7 @@ mod tests { .get_term() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -1488,7 +2017,7 @@ mod tests { .get_fact() .unwrap_err() { - E::NotNonNegIntFact(i) => i == 5, + NotNonNegIntFact(i) => i == 5, _ => false, } ); @@ -1498,7 +2027,7 @@ mod tests { .get_fact() .unwrap_err() { - E::NotNonNegIntFact(i) => i == 4, + NotNonNegIntFact(i) => i == 4, _ => false, } ); @@ -1582,7 +2111,7 @@ mod tests { .get_fact() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -1591,7 +2120,7 @@ mod tests { .get_fact() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -1601,7 +2130,7 @@ mod tests { .get_fact() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -1793,7 +2322,7 @@ mod tests { .get_exps() .unwrap_err() { - E::ExpDivByZero(i) => i == 6, + ExpDivByZero(i) => i == 6, _ => false, } ); @@ -1803,7 +2332,7 @@ mod tests { .get_exps() .unwrap_err() { - E::ExpIsNotInt(i) => i == 7, + ExpIsNotInt(i) => i == 7, _ => false, } ); @@ -1833,7 +2362,7 @@ mod tests { .get_exps() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -1842,7 +2371,7 @@ mod tests { .get_exps() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -1890,7 +2419,7 @@ mod tests { .get_neg() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -1899,7 +2428,7 @@ mod tests { .get_neg() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -2011,7 +2540,7 @@ mod tests { .get_mults() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -2020,7 +2549,7 @@ mod tests { .get_mults() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -2030,7 +2559,7 @@ mod tests { .get_mults() .unwrap_err() { - E::DivByZero(i) => i == 3, + DivByZero(i) => i == 3, _ => false, } ); @@ -2133,7 +2662,7 @@ mod tests { .get_adds() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -2142,7 +2671,7 @@ mod tests { .get_adds() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -2218,7 +2747,7 @@ mod tests { .evaluate() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -2236,7 +2765,7 @@ mod tests { .evaluate() .unwrap_err() { - E::InvalidQuit => true, + InvalidQuit => true, _ => false, } ); @@ -2245,16 +2774,16 @@ mod tests { .evaluate() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); assert!( match Evaluator::new(b"q", &mut Cache::new(), &mut None, &mut Vec::new()) .evaluate() - .unwrap_err() + .unwrap() { - E::MissingLF => true, + O::Exit => true, _ => false, } ); @@ -2418,7 +2947,7 @@ mod tests { .evaluate() .unwrap_err() { - E::MissingTerm(i) => i == 0, + MissingTerm(i) => i == 0, _ => false, } ); @@ -2427,7 +2956,7 @@ mod tests { .evaluate() .unwrap_err() { - E::TrailingSyms(i) => i == 1, + TrailingSyms(i) => i == 1, _ => false, } ); @@ -2437,7 +2966,7 @@ mod tests { .evaluate() .unwrap_err() { - E::TrailingSyms(i) => i == 1, + TrailingSyms(i) => i == 1, _ => false, } ); @@ -2473,13 +3002,97 @@ mod tests { _ => false, } ); - // Empty input does not cause a panic!. - assert!(match Evaluator::new(&[], &mut cache, &mut prev, &mut exp) - .evaluate() - .unwrap_err() - { - E::MissingLF => true, + assert!( + match Evaluator::new(&[0u8; 0], &mut cache, &mut prev.clone(), &mut exp) + .evaluate() + .unwrap() + { + O::Empty(o) => *o == prev, + _ => false, + } + ); + } + #[test] + #[cfg(feature = "rand")] + fn eval_iter() { + use super::*; + use num_traits::ToPrimitive; + use std::io::{self, BufRead, Error, ErrorKind, Read}; + struct Reader<'a> { + data: &'a [u8], + err: bool, + } + impl<'a> Reader<'a> { + fn new(data: &'a [u8]) -> Self { + Self { data, err: true } + } + } + impl<'a> Read for Reader<'a> { + fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { + if self.err { + self.err = false; + Err(Error::new(ErrorKind::Other, "")) + } else { + let len = usize::min(buf.len(), self.data.len()); + buf[..len].copy_from_slice(&self.data[..len]); + Ok(len) + } + } + } + impl<'a> BufRead for Reader<'a> { + fn fill_buf(&mut self) -> io::Result<&[u8]> { + if self.err { + self.err = false; + Err(Error::new(ErrorKind::Other, "")) + } else { + Ok(self.data) + } + } + fn consume(&mut self, amt: usize) { + self.data = &self.data[amt..]; + } + } + let mut iter = EvalIter::new(Reader::new( + b"1+2\n4\n\nq\n5\ns\nrand() + rand(-139/@, 2984/134)\nab", + )); + assert!(match iter.lend_next().unwrap().unwrap_err() { + E::Error(_) => true, + _ => false, + }); + assert!(match iter.lend_next().unwrap().unwrap() { + O::Eval(r) => r.numer().to_i32().unwrap() == 3, + _ => false, + }); + assert!(match iter.lend_next().unwrap().unwrap() { + O::Eval(r) => r.numer().to_i32().unwrap() == 4, + _ => false, + }); + assert!(match iter.lend_next().unwrap().unwrap() { + O::Empty(r) => r.as_ref().unwrap().numer().to_i32().unwrap() == 4, + _ => false, + }); + assert!(iter.lend_next().is_none()); + assert!(match iter.lend_next().unwrap().unwrap() { + O::Eval(r) => r.numer().to_i32().unwrap() == 5, + _ => false, + }); + assert!(match iter.lend_next().unwrap().unwrap() { + O::Store(r) => r.as_ref().unwrap().numer().to_i32().unwrap() == 5, + _ => false, + }); + assert!(match iter.lend_next().unwrap().unwrap() { + O::Eval(r) => r.is_integer(), + _ => false, + }); + assert!(match iter.lend_next().unwrap().unwrap_err() { + E::LangErr(e) => match e { + LangErr::MissingTerm(_) => true, + _ => false, + }, _ => false, }); + assert!(iter.lend_next().is_none()); + assert!(iter.lend_next().is_none()); + assert!(iter.lend_next().is_none()); } } diff --git a/src/main.rs b/src/main.rs @@ -1,13 +1,11 @@ -//! # calc_rational -//! -//! calc_rational consists of a binary crate `calc` and a library crate -//! [`calc_lib`](https://docs.rs/calc_rational/latest/calc_lib). `calc` is a CLI calculator for basic -//! rational number arithmetic using standard operator precedence and associativity. Internally, it is +//! # `calc` +//! `calc` is a CLI calculator for basic rational number arithmetic using standard +//! operator precedence and associativity. Internally, it is //! based on [`Ratio<T>`](https://docs.rs/num/latest/num/rational/struct.Ratio.html) -//! and [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html). -//! -//! ## Calc in action -//! +//! and [`BigInt`](https://docs.rs/num-bigint/latest/num_bigint/struct.BigInt.html). +//! +//! ## Calc in action +//! //! ```bash //! [zack@laptop ~]$ calc //! 2.71828^0^3.14159 + -1! @@ -38,126 +36,109 @@ //! > 2/3 //! //! > 0.666666667 -//! @6 -//! There are only 4 previous results. -//! s 1 -//! Invalid store expression. A store expression must be of the extended regex form: ^[ \t]*s[ \t]*$. -//! q a -//! Invalid quit expression. A quit expression must be of the extended regex form: ^[ \t]*q[ \t]*$. -//! a -//! Missing terminal expression at position 0. A terminal expression is a decimal literal expression, recall expression, absolute value expression, parenthetical expression, or a round expression. -//! |1 -//! Invalid absolute value expression ending at position 2. An absolute value expression is an addition expression enclosed in '||'. -//! (2 -//! Invalid parenthetical expression ending at position 2. A parenthetical expression is an addition expression enclosed in '()'. -//! round(1,10) -//! Invalid round expression ending at position 9. A round expression is of the form 'round(<add expression>, digit)' -//! 1/(2-2) -//! Division by zero ending at position 7. -//! 2^(1/2) -//! Non-integer exponent with a base that was not 0 or 1 ending at position 7. -//! 0^-1 -//! Non-negative exponent with a base of 0 ending at position 4. -//! (-1)! -//! Factorial of a rational number that was not a non-negative integer ending at position 5. -//! 4. -//! Invalid decimal literal expression ending at position 2. A decimal literal expression must be of the extended regex form: [0-9]+(\.[0-9]+)?. -//! 1+2 a -//! Trailing symbols starting at position 4. +//! rand() +//! > 939435294927814822 +//! rand(1+9,10!) +//! > 2660936 //! q //! [zack@laptop ~]$ //! ``` -//! -//! ## Expressions -//! -//! The following are the list of expressions in descending order of precedence: -//! 1. number literals, `@`, `()`, `||`, `round()` -//! 2. `!` -//! 3. `^` -//! 4. `-` (unary negation operator) -//! 5. `*`, `/` -//! 6. `+`, `-` -//! -//! All binary operators are left-associative sans `^` which is right-associative. -//! +//! +//! ## Expressions +//! +//! The following are the list of expressions in descending order of precedence: +//! 1. number literals, `@`, `()`, `||`, `round()`, `rand()` +//! 2. `!` +//! 3. `^` +//! 4. `-` (unary negation operator) +//! 5. `*`, `/` +//! 6. `+`, `-` +//! +//! All binary operators are left-associative sans `^` which is right-associative. +//! //! Any expression is allowed to be enclosed in `()`. Note that parentheses are purely for grouping expressions; //! in particular, you cannot use them to represent multiplication (e.g., `4(2)` is grammatically incorrect and -//! will result in an error message). -//! -//! Any expression is allowed to be enclosed in `||`. This unary operator represents absolute value. -//! +//! will result in an error message). +//! +//! Any expression is allowed to be enclosed in `||`. This unary operator represents absolute value. +//! //! `!` is the factorial operator. Due to its high precedence, something like *-i!^j!* for *i, j ∈ ℕ* is //! the same thing as *-((i!)^(j!))*. If the expression preceding it does not evaluate to a non-negative integer, //! then an error will be displayed. Spaces and tabs are *not* ignored; so `1 !` is grammatically incorrect and -//! will result in an error message. -//! +//! will result in an error message. +//! //! `^` is the exponentiation operator. The expression left of the operator can evaluate to any rational number; //! however the expression right of the operator must evaluate to an integer unless the expression on the left //! evaluates to `0` or `1`. In the event of the former, the expression right of the operator must evaluate to a //! non-negative rational number. In the event of the latter, the expression right of the operator can evaluate to -//! any rational number. Note that `0^0` is defined to be 1. -//! -//! The unary operator `-` represents negation. -//! +//! any rational number. Note that `0^0` is defined to be 1. +//! +//! The unary operator `-` represents negation. +//! //! The operators `*` and `/` represent multiplication and division respectively. Expressions right of `/` -//! must evaluate to any non-zero rational number; otherwise an error will be displayed. -//! -//! The binary operators `+` and `-` represent addition and subtraction respectively. -//! -//! With the aforementioned exception of `!`, all spaces and tabs before and after operators are ignored. -//! -//! ## Round expression -//! +//! must evaluate to any non-zero rational number; otherwise an error will be displayed. +//! +//! The binary operators `+` and `-` represent addition and subtraction respectively. +//! +//! With the aforementioned exception of `!`, all spaces and tabs before and after operators are ignored. +//! +//! ## Round expression +//! //! `round(expression, digit)` rounds `expression` to `digit`-number of fractional digits. An error will -//! be displayed if called incorrectly. -//! -//! ## Numbers -//! +//! be displayed if called incorrectly. +//! +//! ## Rand expression +//! +//! `rand(expression, expression)` generates a random 64-integer inclusively between the passed expressions. +//! An error will be displayed if called incorrectly. `rand()` generates a random 64-bit integer. +//! +//! ## Numbers +//! //! A number literal is a non-empty sequence of digits or a non-empty sequence of digits immediately followed by `.` //! which is immediately followed by a non-empty sequence of digits (e.g., `134.901`). This means that number //! literals represent precisely all possible ratios of non-negative integers to non-negative integers who sole //! prime factors are 2 or 5. To represent all other rational numbers, the unary operator `-` and binary -//! operator `/` must be used. -//! -//! ## Empty expression -//! +//! operator `/` must be used. +//! +//! ## Empty expression +//! //! The empty expression (i.e., expression that at most only consists of spaces and tabs) will return //! the result from the previous non-empty and non-store expression in *decimal* form using the minimum number of digits. //! In the event an infinite number of digits is required, it will be rounded to 9 fractional digits using normal rounding -//! rules first. -//! -//! ## Store expression -//! +//! rules first. +//! +//! ## Store expression +//! //! To store the result of the previous non-empty and non-store expression, one simply passes `s`. In addition to storing the //! result which will subsequently be available via `@`, it displays the result. At most 8 results can be stored at once; -//! at which point, results that are stored overwrite the oldest result. -//! -//! ## Recall expression -//! +//! at which point, results that are stored overwrite the oldest result. +//! +//! ## Recall expression +//! //! `@` is used to recall previously stored results. It can be followed by any *digit* from `1` to `8`. //! If such a digit does not immediately follow it, then it will be interpreted as if there were a `1`. //! `@i` returns the *i*-th most-previous stored result where *i ∈ {1, 2, 3, 4, 5, 6, 7, 8}*. //! Note that spaces and tabs are *not* ignored so `@ 2` is grammatically incorrect and will result in an error message. -//! As emphasized, it does not work on expressions; so both `@@` and `@(1)` are grammatically incorrect. -//! -//! ## Character encoding -//! +//! As emphasized, it does not work on expressions; so both `@@` and `@(1)` are grammatically incorrect. +//! +//! ## Character encoding +//! //! All inputs must only contain the ASCII encoding of the following Unicode scalar values: `0`-`9`, `.`, `+`, `-`, -//! `*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, &lt;line feed&gt;, &lt;carriage return&gt;, -//! `q`, and `d`. Any other byte sequences are grammatically incorrect and will lead to an error message. -//! -//! ## Errors -//! +//! `*`, `/`, `^`, `!`, `|`, `(`, `)`, `round`, `rand`, `,`, `@`, `s`, &lt;space&gt;, &lt;tab&gt;, &lt;line feed&gt;, &lt;carriage return&gt;, +//! and `q`. Any other byte sequences are grammatically incorrect and will lead to an error message. +//! +//! ## Errors +//! //! Errors due to a language violation (e.g., dividing by `0`) manifest into an error message. `panic!`s //! and [`io::Error`](https://doc.rust-lang.org/std/io/struct.Error.html)s caused by writing to the global -//! standard output stream lead to program abortion. -//! -//! ## Exiting -//! -//! `q` with any number of spaces and tabs before and after will cause the program to terminate. -//! -//! ### Formal language specification -//! +//! standard output stream lead to program abortion. +//! +//! ## Exiting +//! +//! `q` with any number of spaces and tabs before and after will cause the program to terminate. +//! +//! ### Formal language specification +//! //! For a more precise specification of the “calc language”, one can read the //! [calc language specification](https://git.philomathiclife.com/calc_rational/lang.pdf). #![deny( @@ -170,50 +151,24 @@ clippy::pedantic, clippy::restriction )] -#![allow(clippy::blanket_clippy_restriction_lints, clippy::implicit_return)] -use calc_lib::cache::Cache; -use calc_lib::Evaluator; -use calc_lib::O::{Empty, Eval, Exit, Store}; -use std::io::{self, BufRead, Error, Write}; +#![allow( + clippy::blanket_clippy_restriction_lints, + clippy::implicit_return, + clippy::question_mark_used +)] +use calc_lib::lending_iterator::LendingIterator; +use calc_lib::EvalIter; +use std::io::{self, Error, Write}; /// Entry point to the calc program. /// # Errors /// -/// Returns [`Error`] iff [`write`] returns one when writing +/// Returns [`Error`] iff [`writeln`] returns one when writing /// to the global standard output stream. -#[allow(clippy::significant_drop_in_scrutinee)] fn main() -> Result<(), Error> { - let mut stored = Cache::new(); - let mut prev = None; - let input = io::stdin(); - let mut in_lock = input.lock(); - let mut buffer = Vec::new(); - let out = io::stdout(); - let mut out_lock = out.lock(); - let mut exp_cache = Vec::new(); - loop { - writeln!( - &mut out_lock, - "{}", - match in_lock.read_until(b'\n', &mut buffer) { - Ok(_) => match Evaluator::new(&buffer, &mut stored, &mut prev, &mut exp_cache) - .evaluate() - { - Ok(o) => match o { - Empty(_) | Eval(_) | Store(_) => o.to_string(), - Exit => return Ok(()), - }, - Err(e) => e.to_string(), - }, - Err(e) => { - let mut err = String::with_capacity(128); - err.push_str("There was an error when reading the passed expression: "); - err.push_str(e.to_string().as_str()); - err - } - } - )?; - buffer.clear(); - exp_cache.clear(); - } + let mut out = io::stdout().lock(); + EvalIter::new(io::stdin().lock()).lend_try_fold((), |_, res| match res { + Ok(o) => writeln!(&mut out, "{o}"), + Err(e) => writeln!(&mut out, "{e}"), + }) }