calc_rational

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

commit 5be9d112bcb3810a2ffd1b661695440ad5f737fd
Author: Zack Newman <zack@philomathiclife.com>
Date:   Thu, 13 Apr 2023 22:34:19 -0600

init

Diffstat:
A.gitignore | 4++++
ACargo.toml | 34++++++++++++++++++++++++++++++++++
ALICENSE-APACHE | 177+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
ALICENSE-MIT | 20++++++++++++++++++++
AREADME.md | 9+++++++++
Alang.pdf | 0
Alang.tex | 86+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/cache.rs | 311+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/lib.rs | 1161+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/main.rs | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
10 files changed, 1969 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,4 @@ +Cargo.lock +target/** +lang.aux +lang.log diff --git a/Cargo.toml b/Cargo.toml @@ -0,0 +1,34 @@ +[package] +authors = ["Zack Newman <zack@philomathiclife.com>"] +categories = ["algorithms", "mathematics", "science", "command-line-utilities"] +description = "CLI calculator for rational numbers." +documentation = "https://docs.rs/calc_rational" +edition = "2021" +keywords = ["calculator", "mathematics", "numerics"] +license = "MIT OR Apache-2.0" +name = "calc_rational" +readme = "README.md" +repository = "https://git.philomathiclife.com/repos/calc_rational/" +version = "0.1.0" + +[lib] +name = "calc_lib" +path = "src/lib.rs" + +[[bin]] +name = "calc" +path = "src/main.rs" + +[dependencies] +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 } + +[badges] +maintenance = { status = "actively-developed" } + +[profile.release] +lto = true +panic = 'abort' +strip = 'symbols' diff --git a/LICENSE-APACHE b/LICENSE-APACHE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/LICENSE-MIT b/LICENSE-MIT @@ -0,0 +1,20 @@ +Copyright © 2023 Zack Newman + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the +“Software”), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be included +in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS +OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md @@ -0,0 +1,9 @@ +calc_rational +---------------- +calc_rational consists of a binary crate calc and a library crate +calc_lib. calc is a CLI calculator that only deals with arithmetic +defined on the rational numbers. 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). +For a formal specification of the calc language, one can read +[lang.pdf](https://git.philomathiclife.com/calc_rational/lang.pdf). diff --git a/lang.pdf b/lang.pdf Binary files differ. diff --git a/lang.tex b/lang.tex @@ -0,0 +1,86 @@ +\documentclass{article} +\usepackage{amsfonts, amsmath, parskip} +\pagestyle{empty} +\begin{document} +We will define the language, \(L\), of our rational number calculator program. + +Define the set of non-terminal symbols to be +\[ N = \{expr, add, mult, neg, exp, fact, term, s, dec, int, at, digit, prev\}. \] +Define the set of terminal symbols to be +\[ \Sigma = \{0,1,2,3,4,5,6,7,8,9,.,+,-,*,/,\string^,!,|(,),@,=,space,lf,q\}. \] +Define the production rules, \(P\), as the following: +\begin{enumerate} + \item \(expr \rightarrow s\ add\ s\ lf\ |\ s\ = \ s\ at \ s\ lf\ |\ s\ = \ s\ digit\ at \ s\ lf\ |\ s\ lf\ |\ s\ q\ s\ lf\) + \item \(add \rightarrow add\ s\ +\ s\ mult\ |\ add\ s\ -\ s\ mult\ |\ mult\) + \item \(mult \rightarrow mult\ s\ *\ s\ neg\ |\ mult\ s\ /\ s\ neg\ |\ neg\) + \item \(neg \rightarrow -\ s\ neg\ |\ exp\) + \item \(exp \rightarrow fact\ s\ \string^\ s\ neg\ |\ fact\) + \item \(fact \rightarrow fact!\ |\ term\) + \item \(term \rightarrow dec\ |\ at\ |\ |s\ add\ s|\ |\ (s\ add\ s)\) + \item \(s \rightarrow space\ s\ |\ \epsilon\) + \item \(dec \rightarrow int\ |\ int.int\) + \item \(int \rightarrow digit\ |\ digit\ int\) + \item \(at \rightarrow @\ |\ @prev\) + \item \(digit \rightarrow prev\ |\ 0\ |\ 9\) + \item \(prev \rightarrow 1\ |\ 2\ |\ 3\ |\ 4\ |\ 5\ |\ 6\ |\ 7\ |\ 8\) +\end{enumerate} +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). \] +Let \(\mathcal{L}(G)\) be the language generated from \(G\). Let \(@ = @1\), and \(@prev\) represent the +\(prev^{th}\) most-recent result. \(lf\) is the Unicode scalar value U+000A, \(space\) is the Unicode scalar value +U+0020, and \(\epsilon\) is the empty string. We 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 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. 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 +expressions have been evaluated. From the above grammar, we see the operator precedence in descending order is the +following: +\begin{enumerate} + \item \(()\), \(||\) + \item \(!\) + \item \(\string^\) + \item \(-\) (the unary negation operator) + \item \(*\), \(/\) + \item \(+\), \(-\) +\end{enumerate} +with \(\string^\) being right-associative and the rest of the binary operators being left-associative. Last, +for \(j \in \mathbb{N}\) and \(d_{j} \in \{0,1,2,3,4,5,6,7,8,9\}\subset \mathbb{Z}\), we have +\begin{align*} +d_{0}d_{1}\cdots d_{n}.d_{n+1}\cdots d_{n+i} = (&d_{0}*10^{n} + d_{1}*10^{n-1} +\cdots + d_{n}*10^{0}\\ + &+d_{n+1}*10^{-1} +\cdots + d_{n+i}*10^{-i}) +\end{align*} +where for \(k\in\mathbb{N}\) +\[ 10^{k} = \overbrace{10 * 10 *\cdots *10}^{k} \] +and for \(l\in\mathbb{Z}^{-}\) +\[ 10^{l} = \overbrace{1/10 * 1/10 *\cdots *1/10}^{|l|}. \] +As a consequence of above, we have the following example: +\[ 1/1.5 = 1/(3/2) = 2/3 \neq 1/6 = 1/3/2. \] +For \(n\in\mathbb{N}\) we define the factorial operator as +\[ n! = n * (n - 1) *\cdots * 1 \] +which of course equals 1 when \(n = 0\). + +For the empty expression and the exit (i.e., \(q\)) and “recall” statements (i.e., statements that have \(=\)), +the previous results are left in tact; all other expressions push the evaluated result to be the next previous +result. Recall statements are used purely to display a previous value with the option to round to \(digit\) number +of fractional digits using normal rounding rules. For example, +\begin{align*} +&4\\ +&@\\ +&4+@2 +\end{align*} +returns 4, stores 4 as the previous result, returns 4, pushes 4 to be the second-most previous result, pushes +4 to be the previous result, returns 8, pushes 4 to be the third-most previous result, pushes 4 to be the +second-most previous result, and pushes 8 to be the most previous result. In contrast, +\begin{align*} +&4\\ +&=@\\ +&4+@2 +\end{align*} +returns 4, stores 4 as the previous result, returns 4, and fails since the last line is not part of our +language \(L\) since there is no second-previous result. +\end{document} diff --git a/src/cache.rs b/src/cache.rs @@ -0,0 +1,311 @@ +#![deny( + unsafe_code, + unused, + warnings, + clippy::all, + clippy::cargo, + clippy::nursery, + clippy::pedantic +)] +#![allow( + clippy::arithmetic_side_effects, + clippy::implicit_return, + clippy::integer_arithmetic +)] +use core::ops::Index; +/// A cache of `N` `T`s. When the cache is filled up, +/// a new entry overwrites the oldest entry. `Cache<T, N>` +/// cannot be expanded or shrunk. +pub struct Cache<T, const N: usize> { + /// The cache of `T`s. + vals: [T; N], + /// The number of `T`s in our cache. Once + /// `N` number of `T`s have been cached, this will + /// always return `N`. + len: usize, + /// The index position of the next insert. + next_head: usize, +} +impl<T, const N: usize> Cache<T, N> { + /// Returns the number of items cached. + /// This will always be `N` once at least + /// `N` items have been cached. + #[inline] + pub const fn len(&self) -> usize { + self.len + } + /// Returns true iff no items have been cached. + #[inline] + pub const fn is_empty(&self) -> bool { + self.len == 0 + } +} +/// Implements the functions `get`, `get_unsafe`, and `push` as well as `Index<usize, Output = T>` +/// for each of the passed usize literals. +/// Only powers of two are allowed to be passed due to how the above functions are implemented. +/// If any other value is passed, the implementation of those functions will be wrong. +macro_rules! cache { + ( $( $x:literal),* ) => { + $( +impl<T> Cache<T, $x> { + /// Returns `Some(&T)` iff there is an item located + /// at index `idx`. Indexing begins at the newest + /// entry (i.e., `self.get(0)` will return the most recent entry + /// iff at least one item has been cached). + #[inline] + pub fn get(&self, idx: usize) -> Option<&T> { + (idx < self.len).then(|| self.vals.index(self.next_head.wrapping_sub(1).wrapping_sub(idx) & ($x - 1))) + } + /// Returns a `&T` at index position `idx % N`. In the event + /// `(idx % N) >= self.len()`, then `&T` references the + /// default value of `T` that was inserted at creation but *not* + /// actually inserted via `push`. + + /// # Safety + /// + /// `idx < self.len()`. + #[inline] + #[allow(unsafe_code)] + pub unsafe fn get_unsafe(&self, idx: usize) -> &T { + self.vals.index(self.next_head.wrapping_sub(1).wrapping_sub(idx) & ($x - 1)) + } + /// Adds `val` to the cache. In the event `N` values have already been cached, + /// the oldest entry is overwritten. Returns a shared reference to the + /// value it inserts. + #[inline] + #[allow(clippy::indexing_slicing)] + pub fn push(&mut self, val: T) -> &T { + if self.len < $x { + self.len += 1; + } + // next_head is always less than or equal to N since we only implement + // this function when N is a power of 2. + self.vals[self.next_head] = val; + let ret = self.vals.index(self.next_head); + self.next_head = (self.next_head + 1) & ($x - 1); + ret + } +} +impl<T> Index<usize> for Cache<T, $x> { + type Output = T; + #[inline] + fn index(&self, index: usize) -> &Self::Output { + self.get(index).unwrap() + } +} + )* + }; +} +// MUST only pass powers of two! Anything else will lead to wrong code. +cache!( + 0x1, + 0x2, + 0x4, + 0x8, + 0x10, + 0x20, + 0x40, + 0x80, + 0x100, + 0x200, + 0x400, + 0x800, + 0x1000, + 0x2000, + 0x4000, + 0x8000, + 0x0001_0000, + 0x0002_0000, + 0x0004_0000, + 0x0008_0000, + 0x0010_0000, + 0x0020_0000, + 0x0040_0000, + 0x0080_0000, + 0x0100_0000, + 0x0200_0000, + 0x0400_0000, + 0x0800_0000, + 0x1000_0000, + 0x2000_0000, + 0x4000_0000, + 0x8000_0000, + 0x0001_0000_0000, + 0x0002_0000_0000, + 0x0004_0000_0000, + 0x0008_0000_0000, + 0x0010_0000_0000, + 0x0020_0000_0000, + 0x0040_0000_0000, + 0x0080_0000_0000, + 0x0100_0000_0000, + 0x0200_0000_0000, + 0x0400_0000_0000, + 0x0800_0000_0000, + 0x1000_0000_0000, + 0x2000_0000_0000, + 0x4000_0000_0000, + 0x8000_0000_0000, + 0x0001_0000_0000_0000, + 0x0002_0000_0000_0000, + 0x0004_0000_0000_0000, + 0x0008_0000_0000_0000, + 0x0010_0000_0000_0000, + 0x0020_0000_0000_0000, + 0x0040_0000_0000_0000, + 0x0080_0000_0000_0000, + 0x0100_0000_0000_0000, + 0x0200_0000_0000_0000, + 0x0400_0000_0000_0000, + 0x0800_0000_0000_0000, + 0x1000_0000_0000_0000, + 0x2000_0000_0000_0000, + 0x4000_0000_0000_0000, + 0x8000_0000_0000_0000 +); +impl<T, const N: usize> Cache<T, N> +where + [T; N]: Default, +{ + /// Returns an instance of itself pre-loaded with + /// the `N` instances of the default value of `T`. + /// Note these default instances are treated as though + /// they don't exist (i.e., `self.is_empty()` will return true). + #[inline] + #[must_use] + pub fn new() -> Self { + Self::default() + } +} +impl<T, const N: usize> Default for Cache<T, N> +where + [T; N]: Default, +{ + #[inline] + fn default() -> Self { + Self { + vals: Default::default(), + len: 0, + next_head: 0, + } + } +} +#[cfg(test)] +mod tests { + use super::Cache; + #[test] + fn test_len() { + let mut c = Cache::<bool, 1>::new(); + assert_eq!(0, c.len()); + c.push(false); + assert_eq!(1, c.len()); + c.push(false); + assert_eq!(1, c.len()); + } + #[test] + fn test_is_empty() { + let mut c = Cache::<bool, 1>::new(); + assert!(c.is_empty()); + c.push(false); + assert!(!c.is_empty()); + } + #[test] + fn test_get() { + let mut c = Cache::<bool, 4>::new(); + assert!(c.get(0).is_none()); + assert!(c.get(1).is_none()); + assert!(c.get(2).is_none()); + assert!(c.get(3).is_none()); + assert!(c.get(4).is_none()); + assert!(c.get(5).is_none()); + assert!(c.get(usize::MAX).is_none()); + c.push(true); + assert!(c.get(0).unwrap()); + assert!(c.get(1).is_none()); + assert!(c.get(2).is_none()); + assert!(c.get(3).is_none()); + assert!(c.get(4).is_none()); + assert!(c.get(5).is_none()); + assert!(c.get(usize::MAX).is_none()); + c.push(false); + assert!(!c.get(0).unwrap()); + assert!(c.get(1).unwrap()); + assert!(c.get(2).is_none()); + assert!(c.get(3).is_none()); + assert!(c.get(4).is_none()); + assert!(c.get(5).is_none()); + assert!(c.get(usize::MAX).is_none()); + c.push(false); + assert!(!c.get(0).unwrap()); + assert!(!c.get(1).unwrap()); + assert!(c.get(2).unwrap()); + assert!(c.get(3).is_none()); + assert!(c.get(4).is_none()); + assert!(c.get(5).is_none()); + assert!(c.get(usize::MAX).is_none()); + c.push(true); + assert!(c.get(0).unwrap()); + assert!(!c.get(1).unwrap()); + assert!(!c.get(2).unwrap()); + assert!(c.get(3).unwrap()); + assert!(c.get(4).is_none()); + assert!(c.get(5).is_none()); + assert!(c.get(usize::MAX).is_none()); + c.push(true); + assert!(c.get(0).unwrap()); + assert!(c.get(1).unwrap()); + assert!(!c.get(2).unwrap()); + assert!(!c.get(3).unwrap()); + assert!(c.get(4).is_none()); + assert!(c.get(5).is_none()); + assert!(c.get(usize::MAX).is_none()); + } + #[test] + #[allow(unsafe_code)] + fn test_get_unsafe() { + let mut c = Cache::<bool, 4>::new(); + unsafe { + assert!(!c.get_unsafe(0)); + assert!(!c.get_unsafe(1)); + assert!(!c.get_unsafe(2)); + assert!(!c.get_unsafe(3)); + assert!(!c.get_unsafe(4)); + } + c.push(true); + unsafe { + assert!(c.get_unsafe(0)); + assert!(!c.get_unsafe(1)); + assert!(!c.get_unsafe(2)); + assert!(!c.get_unsafe(3)); + assert!(c.get_unsafe(4)); + } + } + #[test] + fn test_index() { + let mut c = Cache::<bool, 4>::new(); + c.push(true); + assert!(c[0]); + } + #[test] + #[should_panic] + fn test_index_panic() { + let c = Cache::<bool, 4>::new(); + assert!(c[0]); + } + #[test] + fn test_push() { + let mut c = Cache::<bool, 4>::new(); + assert!(c.push(true)); + assert!(c.push(true)); + assert!(!c.push(false)); + assert!(c.push(true)); + assert!(!c.push(false)); + assert!(!c.push(false)); + } + #[test] + fn test_new() { + _ = Cache::<bool, 0>::new(); + _ = Cache::<bool, 32>::new(); + _ = Cache::<bool, 31>::new(); + } +} diff --git a/src/lib.rs b/src/lib.rs @@ -0,0 +1,1161 @@ +//! This crate contains logic for an application to parse and evaluate +//! basic rational number arithmetic using standard operator precedence and +//! associativity. +#![no_std] +#![deny( + unsafe_code, + unused, + warnings, + clippy::all, + clippy::cargo, + clippy::nursery, + clippy::pedantic +)] +#![allow( + clippy::arithmetic_side_effects, + clippy::exhaustive_enums, + clippy::implicit_return, + clippy::integer_arithmetic, + clippy::into_iter_on_ref, + clippy::question_mark, + clippy::single_char_lifetime_names, + clippy::unseparated_literal_suffix +)] +extern crate alloc; +use alloc::string::{String, ToString}; +use alloc::vec; +use alloc::vec::Vec; +use cache::Cache; +use core::convert; +use core::fmt::{self, Display, Formatter}; +use num_bigint::{BigInt, BigUint, Sign}; +use num_rational::Ratio; +use num_integer::Integer; +use num_traits::Pow; +use E::{ + DivByZero, ExpDivByZero, ExpIsNotInt, InvalidAbs, InvalidDec, InvalidPar, InvalidQuit, + InvalidRecall, MissingLF, MissingTerm, NotEnoughPrevResults, NotNonNegIntFact, TrailingSyms, +}; +use O::{Empty, Exit, Prev, Val}; +/// Fixed-sized cache that automatically overwrites the oldest data +/// when a new item is added and the cache is full. One can think of +/// [`Cache`] as a very limited but more performant [`VecDeque`][alloc::collections::VecDeque] that only +/// adds new data or reads old data. +pub mod cache; +/// A value from a previous expression that is to be +/// displayed potentially in decimal form. +#[derive(Debug)] +pub struct PrevVal<'a> { + /// A cached value from a previous expression. + val: &'a Ratio<BigInt>, + /// None iff the previous value is to be displayed + /// in fractional form; otherwise contains the number + /// of fractional digits from 0 to 9 `val` will be rounded + /// to. + round: Option<u8>, +} +impl<'a> Display for PrevVal<'a> { + #[inline] + #[allow(unsafe_code, clippy::as_conversions, clippy::indexing_slicing)] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + if let Some(rem) = self.round { + let r = rem as usize; + let mult = BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10])).pow(r); + // int < 0 iff val <= -1. frac < 0 iff val is a negative non-integer. + let (int, frac) = (self.val * &mult).round().numer().div_rem(&mult); + let int_str = int.to_string().into_bytes(); + let frac_str = frac.to_string().into_bytes(); + // The max length is the length of int_str plus the number of fractional digits + // plus a possible negative sign plus a possible decimal point. + // At most we will over-allocate by 2 precisely when int is negative + // and r is 0. + let mut v = Vec::with_capacity(int_str.len() + 2 + r); + // In the case val is a negative value greater than -1, + // we have to add the negative sign since 0 is unsigned. + // Technically this can panic! if one of the Strings returned from + // to_string is empty; however that is a bug, or at least undesired + // behavior, in the to_string implementation from BigInt. + let frac_val = match (int_str[0] == b'-', frac_str[0] == b'-') { + (false, true) => { + v.push(b'-'); + &frac_str[1..] + } + (true, true) => &frac_str[1..], + _ => frac_str.as_slice(), + }; + v.extend_from_slice(int_str.as_slice()); + if r > 0 { + v.push(b'.'); + let len = v.len(); + // frac_val.len() is <= r. + while v.len() < len + r - frac_val.len() { + v.push(b'0'); + } + v.extend_from_slice(frac_val); + } + // SAFETY: + // v contains precisely the UTF-8 code units returned from Strings + // returned from the to_string function on the integer and fraction part of + // val plus optionally the single byte encodings of ".", "-", and "0". + writeln!(f, "> {}", unsafe { String::from_utf8_unchecked(v) }) + } else { + writeln!(f, "> {}", self.val) + } + } +} +/// 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, + /// The input began with a `q` but had other non-spaces + /// that followed it. + InvalidQuit, + /// The input began with an `=` but + /// was otherwise syntactically invalid. + InvalidRecall, + /// A sub-expression in the input would have led + /// to a division by zero. + DivByZero(usize), + /// A sub-expression in the input would have led + /// to a rational number that was not 0 or 1 to be + /// raised to a non-integer power. + ExpIsNotInt(usize), + /// A sub-expression in the input would have led + /// to 0 being raised to a negative power which itself + /// would have led to a division by zero. + ExpDivByZero(usize), + /// A sub-expression in the input would have led + /// to a non-integer factorial or a negative integer factorial. + NotNonNegIntFact(usize), + /// The input contained a non-empty sequence of digits followed + /// by `.` which was not followed by a non-empty sequence of digits. + InvalidDec(usize), + /// A recall expression was used to recall the *i*-th previous result, + /// but there are fewer than *i* previous results where + /// *i ∈ {1, 2, 3, 4, 5, 6, 7, 8}*. + NotEnoughPrevResults(u8), + /// The input did not contain a closing `|`. + InvalidAbs(usize), + /// The input did not contain a closing `)`. + InvalidPar(usize), + /// A sub-expression in the input had a missing terminal expression + /// where a terminal expression is a decimal literal expression, + /// recall expression, absolute value expression, or a parenthetical + /// expression. + MissingTerm(usize), + /// The input started with a valid expression but was immediately followed + /// by symbols that could not be chained with the preceding expression. + TrailingSyms(usize), +} +impl Display for E { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + MissingLF => f.write_str("The last character is not a line feed.\n"), + InvalidRecall => f.write_str("Invalid recall statement. A recall statement must be of the extended regex form: ^ *= *[0-9]?@[1-8]? *$.\n"), + InvalidQuit => f.write_str("Invalid quit statement. A quit statement must be of the extended regex form: ^ *q *$.\n"), + DivByZero(u) => writeln!(f, "Division by zero ending at position {u}."), + ExpIsNotInt(u) => writeln!(f, "Non-integer exponent with a base that was not 0 or 1 ending at position {u}."), + ExpDivByZero(u) => writeln!(f, "Non-negative exponent with a base of 0 ending at position {u}."), + NotNonNegIntFact(u) => writeln!(f, "Factorial of a rational number that was not a non-negative integer ending at position {u}."), + InvalidDec(u) => writeln!(f, "Invalid decimal literal expression ending at position {u}. A decimal literal expression must be of the extended regex form: *[0-9]+(\\.[0-9]+)?."), + NotEnoughPrevResults(len) => writeln!(f, "There are only {len} previous results."), + InvalidAbs(u) => writeln!(f, "Invalid absolute value expression ending at position {u}. An absolute value expression is an addition expression enclosed in '||'."), + InvalidPar(u) => writeln!(f, "Invalid parenthetical expression ending at position {u}. A parenthetical expression is an addition expression enclosed in '()'."), + MissingTerm(u) => writeln!(f, "Missing terminal expression at position {u}. A terminal expression is a decimal literal expression, recall expression, or addition expression enclosed in vertical bars or parentheses."), + TrailingSyms(u) => writeln!(f, "Trailing symbols starting at position {u}."), + } + } +} +/// A successful evaluation of an input. +#[derive(Debug)] +pub enum O<'a> { + /// The result of a passed expression. + Val(&'a Ratio<BigInt>), + /// The previous result of the requested + /// recall statement. + Prev(PrevVal<'a>), + /// The input contained all spaces which is part of our language + /// but does not do anything. + Empty, + /// A quit statement was issued to terminate the program. + Exit, +} +impl<'a> Display for O<'a> { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Val(r) => writeln!(f, "> {r}"), + Prev(ref p) => p.fmt(f), + Empty | Exit => Ok(()), + } + } +} +/// Evaluates the supplied input. +pub struct Evaluator<'input, 'cache, 'exp> { + /// The input to be evaluated. + utf8: &'input [u8], + /// The index within `utf8` that evaluation needs to continue. + /// We use this instead of slicing from `utf8` since we want + /// to be able to report the position within the input + /// that an error occurs. + i: usize, + /// The cache of previously evaluated expressions. + prev: &'cache mut Cache<Ratio<BigInt>, 8>, + /// Buffer used to evaluate exponentiation sub-expressions. + /// We require buffering exponentiation sub-expressions due + /// to their being right-associative. Instead of repeatedly + /// creating temporary internal buffers, we rely on a supplied + /// buffer that can be re-used across multiple evaluations. + exp_cache: &'exp mut Vec<Ratio<BigInt>>, +} +impl<'input, 'cache, 'exp> Evaluator<'input, 'cache, 'exp> { + /// Creates an `Evaluator<'input, 'cache, 'exp>` based on the supplied arguments. + #[inline] + pub fn new( + utf8: &'input [u8], + prev: &'cache mut Cache<Ratio<BigInt>, 8>, + exp_cache: &'exp mut Vec<Ratio<BigInt>>, + ) -> Evaluator<'input, 'cache, 'exp> { + Self { + utf8, + i: 0, + prev, + exp_cache, + } + } + /// Evaluates the input consuming the `Evaluator<'input, 'cache, 'exp>`. + + /// # Errors + /// + /// Returns an [`E`] iff the input violates the calc language. + #[inline] + #[allow(clippy::indexing_slicing)] + pub fn evaluate(mut self) -> Result<O<'cache>, E> { + self.utf8 = if self.utf8.last().map_or(true, |b| *b != b'\n') { + return Err(MissingLF) + } else { + &self.utf8[..(self.utf8.len() - 1)] + }; + self.consume_ws(); + let Some(b) = self.utf8.get(self.i) else { + return Ok(Empty) + }; + if *b == b'=' { + self.i += 1; + self.consume_ws(); + let Some(b2) = self.utf8.get(self.i) else { + return Err(InvalidRecall) + }; + let b3 = *b2; + let round = b3.is_ascii_digit().then(|| { + self.i += 1; + b3 - b'0' + }); + self.get_recall().map(|val| Prev(PrevVal { val, round })) + } else if *b == b'q' { + self.i += 1; + self.consume_ws(); + if self.i == self.utf8.len() { + Ok(Exit) + } else { + Err(InvalidQuit) + } + } else { + self.get_adds().and_then(move |r| { + self.consume_ws(); + if self.i == self.utf8.len() { + Ok(Val(self.prev.push(r))) + } else { + Err(TrailingSyms(self.i)) + } + }) + } + } + /// Reads from the input until the next non-space byte value. + #[inline] + #[allow(clippy::indexing_slicing)] + fn consume_ws(&mut self) { + // 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 + // Err taking the role of ControlFlow::Break. + self.i += self.utf8[self.i..] + .into_iter() + .try_fold(0, |val, b| { + if *b == b' ' { + Ok(val + 1) + } else { + // Only an "error" in the sense + // we want try_fold to terminate. + Err(val) + } + }) + .map_or_else(convert::identity, convert::identity); + } + /// Evaluates addition expressions as defined in the calc language. + /// 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> { + let mut left = self.get_mults()?; + let mut j; + self.consume_ws(); + while let Some(i) = self.utf8.get(self.i) { + j = *i; + self.consume_ws(); + if j == b'+' { + self.i += 1; + self.consume_ws(); + left += self.get_mults()?; + } else if j == b'-' { + self.i += 1; + self.consume_ws(); + left -= self.get_mults()?; + } else { + break; + } + } + Ok(left) + } + /// Evaluates multiplication expressions as defined in the calc language. + /// 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> { + let mut left = self.get_neg()?; + let mut right; + let mut j; + self.consume_ws(); + while let Some(i) = self.utf8.get(self.i) { + j = *i; + self.consume_ws(); + if j == b'*' { + self.i += 1; + self.consume_ws(); + left *= self.get_neg()?; + } else if j == b'/' { + self.i += 1; + self.consume_ws(); + right = self.get_neg()?; + if right.numer().sign() == Sign::NoSign { + return Err(DivByZero(self.i)); + } + left /= right; + } else { + break; + } + } + Ok(left) + } + /// 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> { + let mut count = 0usize; + while let Some(b) = self.utf8.get(self.i) { + if *b == b'-' { + self.i += 1; + self.consume_ws(); + count += 1; + } else { + break; + } + } + self.get_exps() + .map(|val| if count & 1 == 0 { val } else { -val }) + } + /// Evaluates exponentiation expressions as defined in the calc language. + /// 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> { + let mut t = self.get_fact()?; + let ix = self.exp_cache.len(); + let mut prev; + let mut numer; + self.exp_cache.push(t); + self.consume_ws(); + let mut j; + while let Some(i) = self.utf8.get(self.i) { + j = *i; + self.consume_ws(); + if j == b'^' { + self.i += 1; + self.consume_ws(); + t = self.get_neg()?; + // This is safe since we always push a value into it + // before entering the loop. + prev = self.exp_cache.last().unwrap(); + numer = prev.numer(); + // Equiv to checking if prev is 0. + if numer.sign() == Sign::NoSign { + if t.numer().sign() == Sign::Minus { + return Err(ExpDivByZero(self.i)); + } + self.exp_cache.push(t); + } else if prev.is_integer() { + if numer == &BigInt::new(Sign::Plus, vec![1]) { + // 1 raised to anything is 1, so we don't bother + // storing the exponent. + } else if t.is_integer() { + self.exp_cache.push(t); + } else { + return Err(ExpIsNotInt(self.i)); + } + } else if t.is_integer() { + self.exp_cache.push(t); + } else { + return Err(ExpIsNotInt(self.i)); + } + } else { + break; + } + } + self.exp_cache.drain(ix..).try_rfold( + Ratio::from_integer(BigInt::new(Sign::Plus, vec![1])), + |exp, base| { + if exp.is_integer() { + Ok(base.pow(exp.numer())) + } else if base.numer().sign() == Sign::NoSign { + Ok(base) + } else { + Err(ExpIsNotInt(self.i)) + } + }, + ) + } + /// 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> { + /// Calculates the factorial of `val`. + #[inline] + fn fact(mut val: BigUint) -> BigUint { + let zero = BigUint::new(Vec::new()); + let one = BigUint::new(vec![1]); + let mut calc = BigUint::new(vec![1]); + while val > zero { + calc *= &val; + val -= &one; + } + calc + } + let t = self.get_term()?; + let Some(b) = self.utf8.get(self.i) else { + return Ok(t) + }; + if *b == b'!' { + self.i += 1; + if t.is_integer() { + // We can make a copy of self.i here, or call map_or instead + // of map_or_else. + let i = self.i; + t.numer() + .to_biguint() + .map_or_else(|| Err(NotNonNegIntFact(i)), |val| { + let mut factorial = fact(val); + while let Some(b2) = self.utf8.get(self.i) { + if *b2 == b'!' { + self.i += 1; + factorial = fact(factorial); + } else { + break; + } + } + Ok(Ratio::from_integer(BigInt::from_biguint( + Sign::Plus, + factorial, + ))) + }) + } else { + Err(NotNonNegIntFact(self.i)) + } + } else { + Ok(t) + } + } + /// Evaluates terminal expressions as defined in the calc language. + /// This function is based on parenthetical expressions, absolute value + /// expressions, recall expressions, and numer literal expressions. + #[inline] + #[allow(clippy::option_if_let_else)] + fn get_term(&mut self) -> Result<Ratio<BigInt>, E> { + match self.get_rational() { + Ok(o) => match o { + Some(r) => Ok(r), + None => match self.get_prev() { + Ok(o2) => match o2 { + Some(r2) => Ok(r2.clone()), + None => self + .get_abs() + .and_then(|o3| o3.map_or_else(|| self.get_par(), Ok)), + }, + Err(e2) => Err(e2), + }, + }, + Err(e) => Err(e), + } + } + /// Evaluates parenthetical expressions as defined in the calc language. + /// 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`. + #[inline] + fn get_par(&mut self) -> Result<Ratio<BigInt>, E> { + // 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) else { + return Err(MissingTerm(self.i)); + }; + // This is always the last terminal expression we call; so if there is a missing open parenthesis, + // then the sentence is grammatically incorrect. + if *b == b'(' { + self.i += 1; + self.consume_ws(); + let r = self.get_adds()?; + self.consume_ws(); + let Some(b2) = self.utf8.get(self.i) else { + return Err(InvalidPar(self.i)) + }; + let b3 = *b2; + if b3 == b')' { + self.i += 1; + Ok(r) + } else { + Err(InvalidPar(self.i)) + } + } else { + Err(MissingTerm(self.i)) + } + } + /// 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> { + let Some(b) = self.utf8.get(self.i) else { + return Ok(None) + }; + if *b == b'|' { + self.i += 1; + self.consume_ws(); + let r = self.get_adds()?; + self.consume_ws(); + let Some(b2) = self.utf8.get(self.i) else { + return Err(InvalidAbs(self.i)) + }; + let b3 = *b2; + if b3 == b'|' { + self.i += 1; + Ok(Some(if r.numer().sign() == Sign::Minus { + -r + } else { + r + })) + } else { + Err(InvalidAbs(self.i)) + } + } else { + Ok(None) + } + } + /// Obtains the index within `self.prev` that a recall + /// expression is to return. This is a separate function + /// from [`get_prev`] due to [`get_recall`] also using this + /// logic. + #[inline] + #[allow(clippy::as_conversions)] + fn get_prev_idx(&mut self) -> Option<usize> { + let Some(b) = self.utf8.get(self.i) else { + return None + }; + (*b == b'@').then(|| { + self.i += 1; + self.utf8.get(self.i).map_or(0, |b2| { + if (b'1'..b'9').contains(b2) { + self.i += 1; + (*b2 - b'1') as usize + } else { + 0 + } + }) + }) + } + /// Evaluates recall expressions as defined in the calc language. + #[inline] + #[allow(clippy::as_conversions, clippy::cast_possible_truncation)] + fn get_prev(&mut self) -> Result<Option<&Ratio<BigInt>>, E> { + self.get_prev_idx().map_or_else(|| Ok(None), |i| { + self.prev + .get(i) + .map_or_else(|| Err(NotEnoughPrevResults(self.prev.len() as u8)), |r| Ok(Some(r))) + }) + } + /// Evaluates recall statements as defined in the calc language. + /// This function has to be separate from [`get_prev`] since it returns + /// a shared reference with 'cache lifetime which requires the function to move self + /// since it has an exclusive reference to the cache. + #[inline] + #[allow(clippy::as_conversions, clippy::cast_possible_truncation)] + fn get_recall(mut self) -> Result<&'cache Ratio<BigInt>, E> { + self.get_prev_idx().map_or_else(|| Err(InvalidRecall), move |i| { + self.consume_ws(); + self.prev.get(i).map_or_else(|| Err(NotEnoughPrevResults(self.prev.len() as u8)), |val| { + if self.i == self.utf8.len() { + Ok(val) + } else { + Err(InvalidRecall) + } + }) + }) + } + /// Evaluates number literal expressions as defined in the calc language. + #[inline] + #[allow( + clippy::as_conversions, + clippy::cast_lossless, + clippy::indexing_slicing + )] + fn get_rational(&mut self) -> Result<Option<Ratio<BigInt>>, E> { + #[inline] + /// Used to parse a sequence of digits into an unsigned integer. + fn to_biguint(v: &[u8]) -> (BigUint, usize) { + v.into_iter() + .try_fold((BigUint::new(Vec::new()), 0), |mut prev, d| { + if d.is_ascii_digit() { + prev.1 += 1; + prev.0 = prev.0 * 10u8 + (*d - b'0'); + Ok(prev) + } else { + Err(prev) + } + }) + .map_or_else(convert::identity, convert::identity) + } + let (int, len) = to_biguint(&self.utf8[self.i..]); + if len == 0 { + return Ok(None); + } + self.i += len; + if let Some(b) = self.utf8.get(self.i) { + if *b == b'.' { + self.i += 1; + let (numer, len2) = to_biguint(&self.utf8[self.i..]); + if len2 == 0 { + Err(InvalidDec(self.i)) + } else { + self.i += len2; + Ok(Some( + Ratio::from_integer(BigInt::from_biguint(Sign::Plus, int)) + + Ratio::new( + BigInt::from_biguint(Sign::Plus, numer), + BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10]).pow(len2)), + ), + )) + } + } else { + Ok(Some(Ratio::from_integer(BigInt::from_biguint( + Sign::Plus, + int, + )))) + } + } else { + Ok(Some(Ratio::from_integer(BigInt::from_biguint( + Sign::Plus, + int, + )))) + } + } +} +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn prev_string() { + assert!(PrevVal { val: &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![12]))), round: None }.to_string() == "> 12\n"); + assert!(PrevVal { val: &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![12]))), round: Some(0) }.to_string() == "> 12\n"); + assert!(PrevVal { val: &Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![12]))), round: Some(1) }.to_string() == "> -12.0\n"); + assert!(PrevVal { val: &Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))), round: None }.to_string() == "> 2/3\n"); + assert!(PrevVal { val: &Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))), round: Some(0)}.to_string() == "> -1\n"); + assert!(PrevVal { val: &Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))), round: Some(1)}.to_string() == "> -0.7\n"); + assert!(PrevVal { val: &Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![4])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![6]))), round: Some(2)}.to_string() == "> -0.67\n"); + } + #[test] + fn number_literal() { + // Normal 0 is fine. + assert!(Evaluator::new(b"0", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new())))); + // Leading 0s and trailing 0s are fine. + assert!(Evaluator::new(b"0000.00000", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new())))); + // Parsing stops at first non-(digt/period). + assert!(Evaluator::new(b"1 0", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + let int = b"3397450981271938475135134759823759835414"; + let frac = b"913759810573549872354897210539127530981570"; + let left = Ratio::from_integer(BigInt::parse_bytes(int, 10).unwrap()); + let right = Ratio::new(BigInt::parse_bytes(frac, 10).unwrap(), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![10]).pow(frac.len() + 1))); + let mut vec = Vec::new(); + vec.extend_from_slice(int); + vec.push(b'.'); + vec.push(b'0'); + vec.extend_from_slice(frac); + // Test a number whose integer and fraction portions are larger than u128::MAX. + assert!(Evaluator::new(vec.as_slice(), &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == left + right); + // Leading 0s and trailing 0s for a non-zero value are fine. + assert!(Evaluator::new(b"000000014.0000000000000", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![14])))); + // A sequence of digits followed immediately by a decimal point but no digits after is invalid. + assert!(match Evaluator::new(b"1.", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap_err() { + E::InvalidDec(i) => i == 2, + _ => false, + }); + // A sequence of digits followed immediately by a decimal point and space but no digits after is invalid. + // This just shows that spaces are not ignored in number literals. + assert!(match Evaluator::new(b"1. 2", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap_err() { + E::InvalidDec(i) => i == 2, + _ => false, + }); + // A non-space starting the input is valid but produces no value. + // This also shows that an invalid byte sequence does not produce an error here. + assert!(Evaluator::new(b"a1", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().is_none()); + // A space starting the input is valid but produces no value. + assert!(Evaluator::new(b" 1", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().is_none()); + // Negative literals don't exist, so this should succeed but produce nothing. + assert!(Evaluator::new(b"-1", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().is_none()); + // '/' is division and not part of a number literal so only the "numerator" is parsed. + assert!(Evaluator::new(b"1/2", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // A sequence of digits followed by invalid bytes is valid and produces a number equal to the digits before the invalid bytes. + assert!(Evaluator::new(b"130alj", &mut Cache::new(), &mut Vec::new()).get_rational().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![130])))); + } + #[test] + fn recall_expression() { + // If the input does not start with '@', then it's valid but produces nothing. + assert!(Evaluator::new(b"1", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap().is_none()); + assert!(Evaluator::new(b"a", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap().is_none()); + assert!(Evaluator::new(b" @", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap().is_none()); + // Invalid recall expression since there are no previous results. + assert!(match Evaluator::new(b"@", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap_err() { + E::NotEnoughPrevResults(count) => count == 0, + _ => false, + }); + // Invalid recall expression since there are no previous results. + assert!(match Evaluator::new(b"@4", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap_err() { + E::NotEnoughPrevResults(count) => count == 0, + _ => false, + }); + // Invalid recall expression since there are no previous results. + // The input violates our grammar, but this error happens before that. + assert!(match Evaluator::new(b"@0", &mut Cache::new(), &mut Vec::new()).get_prev().unwrap_err() { + E::NotEnoughPrevResults(count) => count == 0, + _ => false, + }); + // Successfully extract previous expression. + let mut cache = Cache::new(); + Evaluator::new(b"1\n", &mut cache, &mut Vec::new()).evaluate().unwrap(); + assert!(Evaluator::new(b"@", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // Invalid characters are ignored at this stage. + assert!(Evaluator::new(b"@&", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // 0 is not a valid previous value only 1-8 are. + assert!(Evaluator::new(b"@0", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // 9 is not a valid previous value only 1-8 are. + assert!(Evaluator::new(b"@9", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // Spaces are not cleaned; otherwise this would error since we only have 1 previous value. + assert!(Evaluator::new(b"@ 2", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // One digits are looked at so this is not @<ten>. + assert!(Evaluator::new(b"@10", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // Invalid recall expression since there are no previous results. + assert!(match Evaluator::new(b"@2", &mut cache, &mut Vec::new()).get_prev().unwrap_err() { + E::NotEnoughPrevResults(count) => count == 1, + _ => false, + }); + Evaluator::new(b"2\n", &mut cache, &mut Vec::new()).evaluate().unwrap(); + // Previous value is correct. + assert!(Evaluator::new(b"@", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])))); + // Second previous value is correct. + assert!(Evaluator::new(b"@2", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // Invalid recall expression since there are no previous results. + assert!(match Evaluator::new(b"@3", &mut cache, &mut Vec::new()).get_prev().unwrap_err() { + E::NotEnoughPrevResults(count) => count == 2, + _ => false, + }); + let mut v = vec![0, b'\n']; + for i in b'3'..=b'8' { + v[0] = i; + Evaluator::new(v.as_slice(), &mut cache, &mut Vec::new()).evaluate().unwrap(); + } + v[0] = b'@'; + for i in b'1'..=b'8' { + v[1] = i; + // Cache is filled up correctly storing all previous values. + assert!(Evaluator::new(v.as_slice(), &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![(b'9' - i) as u32])))); + } + // Only parses the first @ since the second @ is not a digit between 1 and 8. + assert!(Evaluator::new(b"@@", &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8])))); + Evaluator::new(b"9\n", &mut cache, &mut Vec::new()).evaluate().unwrap(); + // Oldest value is overwritten; all others remain. + for i in b'1'..=b'8' { + v[1] = i; + assert!(Evaluator::new(v.as_slice(), &mut cache, &mut Vec::new()).get_prev().unwrap().unwrap() == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![((b'9' + 1) - i) as u32])))); + } + } + #[test] + fn abs() { + // Missing closing '|' + assert!(match Evaluator::new(b"|1", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap_err() { + E::InvalidAbs(i) => i == 2, + _ => false, + }); + assert!(match Evaluator::new(b"||1 + 2|", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap_err() { + E::InvalidAbs(i) => i == 8, + _ => false, + }); + assert!(Evaluator::new(b"| 0 |", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new())))); + assert!(Evaluator::new(b"| - 5 |", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![5])))); + assert!(Evaluator::new(b"| | 2 - 5| * 9 |", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![27])))); + // If the input does not start with '|', then it's valid but produces nothing. + assert!(Evaluator::new(b" |9|", &mut Cache::new(), &mut Vec::new()).get_abs().unwrap().is_none()); + } + #[test] + fn par() { + // Missing closing ')' + assert!(match Evaluator::new(b"(1", &mut Cache::new(), &mut Vec::new()).get_par().unwrap_err() { + E::InvalidPar(i) => i == 2, + _ => false, + }); + assert!(match Evaluator::new(b"((1 + 2)", &mut Cache::new(), &mut Vec::new()).get_par().unwrap_err() { + E::InvalidPar(i) => i == 8, + _ => false, + }); + assert!(Evaluator::new(b"( 0 )", &mut Cache::new(), &mut Vec::new()).get_par().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new())))); + assert!(Evaluator::new(b"( - 5 )", &mut Cache::new(), &mut Vec::new()).get_par().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![5])))); + assert!(Evaluator::new(b"( ( 2 - 5) * 9 )", &mut Cache::new(), &mut Vec::new()).get_par().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![27])))); + // If the input does not start with '(', then it's invalid since get_par must only be called as the last terminal expression which means whitespace must be consumed already. + assert!(match Evaluator::new(b" (2)", &mut Cache::new(), &mut Vec::new()).get_par().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + } + #[test] + fn term() { + assert!(Evaluator::new(b"0000.00000", &mut Cache::new(), &mut Vec::new()).get_term().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new())))); + assert!(Evaluator::new(b"|4|", &mut Cache::new(), &mut Vec::new()).get_term().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])))); + assert!(Evaluator::new(b"(4)", &mut Cache::new(), &mut Vec::new()).get_term().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])))); + // Terminal expressions do no clean up before or after. + assert!(match Evaluator::new(b" 2", &mut Cache::new(), &mut Vec::new()).get_term().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + let mut cache = Cache::new(); + Evaluator::new(b"1\n", &mut cache, &mut Vec::new()).evaluate().unwrap(); + assert!(Evaluator::new(b"@", &mut cache, &mut Vec::new()).get_term().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + } + #[test] + fn factorial() { + // Negative integer is not allowed. + assert!(match Evaluator::new(b"(-1)!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap_err() { + E::NotNonNegIntFact(i) => i == 5, + _ => false, + }); + // Non-integer is not allowed. + assert!(match Evaluator::new(b"2.5!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap_err() { + E::NotNonNegIntFact(i) => i == 4, + _ => false, + }); + // factorials always become terminal expressions eventually. + assert!(Evaluator::new(b"7", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![7])))); + assert!(Evaluator::new(b"(7)", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![7])))); + assert!(Evaluator::new(b"|7|", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![7])))); + let mut cache = Cache::new(); + Evaluator::new(b"3\n", &mut cache, &mut Vec::new()).evaluate().unwrap(); + assert!(Evaluator::new(b"@!", &mut cache, &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![6])))); + assert!(Evaluator::new(b"@", &mut cache, &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3])))); + // 0! = 1. + assert!(Evaluator::new(b"0.0!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // 1! = 1. + assert!(Evaluator::new(b"1!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // 4! = 24, and spaces are not consumed. + assert!(Evaluator::new(b"4! ", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![24])))); + // Factorials can be chained. + assert!(Evaluator::new(b"3!! ", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![720])))); + // only factorial is consumed. + assert!(Evaluator::new(b"2!+3", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])))); + // Error since leading/trailing whitespace is not consumed by factorial or higher precedence expressions. + assert!(match Evaluator::new(b" 2!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + // Error since leading/trailing whitespace is not consumed by factorial or higher precedence expressions. + assert!(match Evaluator::new(b"-2!", &mut Cache::new(), &mut Vec::new()).get_fact().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + } + #[test] + fn exp() { + // 1 can be raised to anything and return 1. + // Also white space is ignored between operator. + assert!(Evaluator::new(b"1 ^ 0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"1^0.5", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"1^(-1/2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"1.0^(-2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + // 0 can be raised to any non-negative value and will always return 0 unless raised to 0 which will return 1. + assert!(Evaluator::new(b"0^0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"0^1", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(vec![0])))); + assert!(Evaluator::new(b"0^0.5", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(vec![0])))); + // Anything else can only be raised to integers. + assert!(Evaluator::new(b"4^0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"4^1", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])))); + assert!(Evaluator::new(b"4^(-2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![16])))); + assert!(Evaluator::new(b"(-4)^0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"(-4)^1", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![4])))); + assert!(Evaluator::new(b"(-4)^2", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![16])))); + assert!(Evaluator::new(b"(-4)^(-2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![16])))); + assert!(Evaluator::new(b"(-4)^(-3)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![1])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![64])))); + assert!(Evaluator::new(b"(2/3)^0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"(2/3)^(2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![9])))); + assert!(Evaluator::new(b"(2/3)^(-3)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![27])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8])))); + assert!(Evaluator::new(b"(-2/3)^0", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"(-2/3)^(2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![9])))); + assert!(Evaluator::new(b"(-2/3)^(3)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![8])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![27])))); + assert!(Evaluator::new(b"(-2/3)^(-2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![9])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])))); + assert!(Evaluator::new(b"(-2/3)^(-3)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![27])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8])))); + // Error since 0 cannot be raised to a negative power. + assert!(match Evaluator::new(b"0^(-1)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap_err() { + E::ExpDivByZero(i) => i == 6, + _ => false, + }); + // Error anything other than 0 or 1 cannot be raised to a non-integer power. + assert!(match Evaluator::new(b"2^(1/2)", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap_err() { + E::ExpIsNotInt(i) => i == 7, + _ => false, + }); + // exps always become factorials eventually. + assert!(Evaluator::new(b"3!", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![6])))); + // exponentiation has lower precedence than factorials. + assert!(Evaluator::new(b"2^3!", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![64])))); + assert!(Evaluator::new(b"3!^2", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![36])))); + // Error since leading/trailing whitespace is not consumed by exponentiation or higher precedence expressions. + assert!(match Evaluator::new(b" 2^2", &mut Cache::new(), &mut Vec::new()).get_exps().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + } + #[test] + fn neg() { + assert!(Evaluator::new(b"-1", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"- - 1", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"-0", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::NoSign, BigUint::new(Vec::new())))); + // negation has lower precedence than exponentiation. + assert!(Evaluator::new(b"-2^2", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![4])))); + // negation always becomes exponentiation eventually. + assert!(Evaluator::new(b"2.0^2.0", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])))); + // Error since leading/trailing whitespace is not consumed by exponentiation or higher precedence expressions. + assert!(match Evaluator::new(b" -2", &mut Cache::new(), &mut Vec::new()).get_neg().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + } + #[test] + fn mult() { + assert!(Evaluator::new(b"2 * 3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![6])))); + assert!(Evaluator::new(b"-2 * 3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![6])))); + assert!(Evaluator::new(b"2 * -3.0", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![6])))); + assert!(Evaluator::new(b"-2.5*-3.5", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![35])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])))); + assert!(Evaluator::new(b"4.0 / 6", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3])))); + assert!(Evaluator::new(b"6/3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])))); + assert!(Evaluator::new(b"-6/3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2])))); + assert!(Evaluator::new(b"6/-3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2])))); + assert!(Evaluator::new(b"- 6 / - 3", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])))); + // Number literals are not strictly equivalent to "ratios" as "ratios" don't exist (i.e., 2/3 is not the ratio of 2 to 3 but is the rational number two divided by the rational number 3). + assert!(Evaluator::new(b"1/1.5", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() != Evaluator::new(b"1/3/2", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap()); + assert!(Evaluator::new(b"1/1.5", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Evaluator::new(b"1/(3/2)", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap()); + // multiplication always becomes negation eventually. + assert!(Evaluator::new(b"-2.0", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2])))); + // Error since leading/trailing whitespace is not consumed by multiplication or higher precedence expressions. + assert!(match Evaluator::new(b" 2*2", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + assert!(match Evaluator::new(b" 2/2", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + // Cannot divide by 0. + assert!(match Evaluator::new(b"2/0", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap_err() { + E::DivByZero(i) => i == 3, + _ => false, + }); + // multiplication has lower precedence than exponentiation. + assert!(Evaluator::new(b"2*2^2", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![8])))); + assert!(Evaluator::new(b"8/2^2", &mut Cache::new(), &mut Vec::new()).get_mults().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2])))); + } + #[test] + fn add() { + assert!(Evaluator::new(b"2 + 3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![5])))); + assert!(Evaluator::new(b"-2 + 3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"2 + -3.0", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![1])))); + assert!(Evaluator::new(b"-2.5+-3.5", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![6])))); + assert!(Evaluator::new(b"4.0 - 6", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2])))); + assert!(Evaluator::new(b"6-3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3])))); + assert!(Evaluator::new(b"-6-3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![9])))); + assert!(Evaluator::new(b"6--3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![9])))); + assert!(Evaluator::new(b"- 6 - - 3", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![3])))); + // addition always becomes multiplication eventually. + assert!(Evaluator::new(b"2 * 8", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![16])))); + assert!(Evaluator::new(b"8 / 2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![4])))); + // Error since leading/trailing whitespace is not consumed by addition or higher precedence expressions. + assert!(match Evaluator::new(b" 2+2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + assert!(match Evaluator::new(b" 2-2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + // addition has lower precedence than multiplication. + assert!(Evaluator::new(b"2+2*2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![6])))); + assert!(Evaluator::new(b"2+2/2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3])))); + assert!(Evaluator::new(b"2-2*2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Minus, BigUint::new(vec![2])))); + assert!(Evaluator::new(b"2-2/2", &mut Cache::new(), &mut Vec::new()).get_adds().unwrap() == Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1])))); + } + #[test] + fn empty() { + assert!(match Evaluator::new(b" \n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap() { + O::Empty => true, + _ => false, + }); + assert!(match Evaluator::new(b"\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap() { + O::Empty => true, + _ => false, + }); + assert!(match Evaluator::new(b"\r\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + assert!(match Evaluator::new(b"\t\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + assert!(match Evaluator::new(b"\n\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + assert!(match Evaluator::new(b"\n ", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() { + E::MissingLF => true, + _ => false, + }); + } + #[test] + fn exit() { + assert!(match Evaluator::new(b" q \n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap() { + O::Exit=> true, + _ => false, + }); + assert!(match Evaluator::new(b"q\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap() { + O::Exit=> true, + _ => false, + }); + assert!(match Evaluator::new(b"\rq\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + assert!(match Evaluator::new(b"\tq\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + assert!(match Evaluator::new(b"q\n\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() { + E::InvalidQuit => true, + _ => false, + }); + assert!(match Evaluator::new(b"\nq\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + assert!(match Evaluator::new(b"q", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() { + E::MissingLF => true, + _ => false, + }); + } + #[test] + fn recall_statement() { + let mut cache = Cache::new(); + Evaluator::new(b"1\n", &mut cache, &mut Vec::new()).evaluate().unwrap(); + assert!(match Evaluator::new(b" = @ \n", &mut cache, &mut Vec::new()).evaluate().unwrap() { + O::Prev(v)=> v.val == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) && v.round.is_none(), + _ => false, + }); + assert!(match Evaluator::new(b" = 2@ \n", &mut cache, &mut Vec::new()).evaluate().unwrap() { + O::Prev(v)=> v.val == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) && v.round.unwrap() == 2, + _ => false, + }); + assert!(match Evaluator::new(b"=9@1\n", &mut cache, &mut Vec::new()).evaluate().unwrap() { + O::Prev(v)=> v.val == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) && v.round.unwrap() == 9, + _ => false, + }); + assert!(match Evaluator::new(b"=0@1\n", &mut cache, &mut Vec::new()).evaluate().unwrap() { + O::Prev(v)=> v.val == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1]))) && v.round.unwrap() == 0, + _ => false, + }); + assert!(match Evaluator::new(b"=@2\n", &mut Cache::new(), &mut Vec::new()).evaluate().unwrap_err() { + E::NotEnoughPrevResults(count)=> count == 0, + _ => false, + }); + assert!(match Evaluator::new(b"=@ 1\n", &mut cache, &mut Vec::new()).evaluate().unwrap_err() { + E::InvalidRecall => true, + _ => false, + }); + assert!(match Evaluator::new(b"=0 @1\n", &mut cache, &mut Vec::new()).evaluate().unwrap_err() { + E::InvalidRecall => true, + _ => false, + }); + assert!(match Evaluator::new(b"=@", &mut cache, &mut Vec::new()).evaluate().unwrap_err() { + E::MissingLF=> true, + _ => false, + }); + } + #[test] + fn eval() { + use core::str::FromStr; + let mut cache = Cache::new(); + let mut exp = Vec::new(); + assert!(match Evaluator::new(b"1+2\n", &mut cache, &mut exp).evaluate().unwrap() { + O::Val(i) => i == &Ratio::from_integer(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![3]))), + _ => false, + }); + assert!(match Evaluator::new(b"-1/2+2*@\n", &mut cache, &mut exp).evaluate().unwrap() { + O::Val(i) => i == &Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![11])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![2]))), + _ => false, + }); + assert!(match Evaluator::new(b"@^@2!\n", &mut cache, &mut exp).evaluate().unwrap() { + O::Val(i) => i == &Ratio::new(BigInt::from_biguint(Sign::Plus, BigUint::new(vec![1771561])), BigInt::from_biguint(Sign::Plus, BigUint::new(vec![64]))), + _ => false, + }); + // Verified with Wolfram Alpha. + assert!(match Evaluator::new(b" 1 + (2 * |(7.98 - 12/7)|) / 4!^@3!^|1-3| \n", &mut cache, &mut exp).evaluate().unwrap() { + O::Val(i) => i == &Ratio::from_str("2841328814244153299237884950647090899374680152474331/2841328814244153299237884950647090899374680152473600").unwrap(), + _ => false, + }); + // Invalid UTF-8 does not cause a panic!. + assert!(match Evaluator::new(&[255, 255, 255, b'\n'], &mut cache, &mut exp).evaluate().unwrap_err() { + E::MissingTerm(i) => i == 0, + _ => false, + }); + assert!(match Evaluator::new(&[b'2', 255, b'\n'], &mut cache, &mut exp).evaluate().unwrap_err() { + E::TrailingSyms(i) => i == 1, + _ => false, + }); + // Exactly one line feed is required. + assert!(match Evaluator::new(&[b'2', b'\n', b'\n'], &mut cache, &mut exp).evaluate().unwrap_err() { + E::TrailingSyms(i) => i == 1, + _ => false, + }); + assert!(match Evaluator::new(&[b'\n'], &mut cache, &mut exp).evaluate().unwrap() { + O::Empty => true, + _ => false, + }); + // Empty input does not cause a panic!. + assert!(match Evaluator::new(&[], &mut cache, &mut exp).evaluate().unwrap_err() { + E::MissingLF => true, + _ => false, + }); + } +} diff --git a/src/main.rs b/src/main.rs @@ -0,0 +1,167 @@ +//! This crate is a CLI calculator for basic rational number arithmetic using standard operator precedence and +//! associativity. Additionally, it stores the previous eight evaluated expressions as well as provides a way to +//! display previous results in decimal form. +//! +//! ```bash +//! 2.71828^0^3.14159 + -1! +//! > 0 +//! @^0 +//! > 1 +//! @/3 * 3 +//! > 1 +//! |@2 - 9|^(1 - 2*3) +//! > 1/32768 +//! =3@ +//! > 0.000 +//! =6@ +//! > 0.000031 +//! ``` +//! +//! # Operators +//! +//! The following are the list of operators in descending order of precedence: +//! 1. `()`, `||` +//! 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 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 before and after operators are ignored. +//! +//! # 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. +//! +//! # Recall expressions +//! +//! `@` is used to recall previously evaluated expressions. 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 result where *i ∈ {1, 2, 3, 4, 5, 6, 7, 8}*. +//! Note that spaces 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. +//! +//! # Displaying results in decimal form +//! +//! All calculations are done using exact rational number arithmetic; however if one wants to display a previous +//! result in decimal form, one can use `=<digit>@<1-8>`. For example `=2@` will display the previous result +//! rounded to two decimal places following normal rounding rules. Note that this is not an expression and so does +//! not influence previous results as can be recalled by `@`. +//! +//! # Character encoding +//! +//! All inputs must only contain the UTF-8 encoding of the following Unicode scalar values: `0`-`9`, `.`, `+`, `-`, +//! `*`, `/`, `^`, `!`, `|`, `(`, `)`, `@`, `=`, &lt;space&gt;, &lt;line feed&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, +//! [`io::Error`][std::io::Error]s that are not of the [`ErrorKind::Interrupted`][std::io::ErrorKind::Interrupted] +//! variant, and [`fmt::Error`][core::fmt::Error]s all lead to program abortion. +//! +//! # Exiting +//! +//! `q` with any number of spaces 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( + unsafe_code, + unused, + warnings, + clippy::all, + clippy::cargo, + clippy::nursery, + clippy::pedantic +)] +#![allow(clippy::implicit_return)] +use calc_lib::cache::Cache; +use calc_lib::Evaluator; +use calc_lib::O::{Empty, Exit, Prev, Val}; +use core::fmt::{self, Debug, Formatter}; +use std::io::{self, BufRead, Error, Write}; +/// Generic unrecoverable error. +enum GenErr { + /// An [`io::Error`][std::io::Error] that is not of the + /// [`io::ErrorKind::Interrupted`][std::io::ErrorKind] variant. + Error(Error), + /// A [`fmt::Error`][core::fmt::Error]. + Fmt(fmt::Error), +} +impl From<Error> for GenErr { + #[inline] + fn from(value: Error) -> Self { + Self::Error(value) + } +} +impl From<fmt::Error> for GenErr { + #[inline] + fn from(value: fmt::Error) -> Self { + Self::Fmt(value) + } +} +impl Debug for GenErr { + #[inline] + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match *self { + Self::Error(ref e) => e.fmt(f), + Self::Fmt(ref e) => e.fmt(f), + } + } +} +/// Entry point to the calc program. +fn main() -> Result<(), GenErr> { + let mut prev = Cache::new(); + 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 { + in_lock.read_until(b'\n', &mut buffer)?; + match Evaluator::new(&buffer, &mut prev, &mut exp_cache).evaluate() { + Ok(o) => match o { + Empty => (), + Exit => return Ok(()), + Prev(_) | Val(_) => out_lock.write_all(&o.to_string().into_bytes())?, + }, + Err(e) => out_lock.write_all(&e.to_string().into_bytes())?, + } + buffer.clear(); + exp_cache.clear(); + } +}