feat: return Result instead of panicking

this doesn't always work still;
there are a lot of panicking index operations there.

a feature was stabilized very recently that would help fix that:
https://github.com/rust-lang/rust/issues/146954
This commit is contained in:
electria 2026-09-30 19:39:34 -07:00
commit 35da9a0d40
Signed by: electria
SSH key fingerprint: SHA256:8LlB3ucPbBHqozqkhsNbaV5oG3SlzzqUj8FZDL6IPQs
5 changed files with 77 additions and 34 deletions

View file

@ -2,9 +2,9 @@ use std::str::FromStr;
use rug::Float;
use crate::nodes::Nodes;
use crate::nodes::{Error, Nodes};
pub fn pretty_eval(mut input: String) -> String {
pub fn pretty_eval(mut input: String) -> Result<String, Error> {
// no need to remove the characters here,
// as we ignore the them in all other logic already
let should_add_back_alignment = input.contains('&');
@ -18,10 +18,10 @@ pub fn pretty_eval(mut input: String) -> String {
})
.is_some();
let result = Nodes::from_str(&input).unwrap().evaluate();
let result = Nodes::from_str(&input)?.evaluate()?;
let significant_digits = get_significant_digits(&result);
format!(
Ok(format!(
"{:leading_spaces$}{}{result:.significant_digits$}{}",
' ',
if should_add_back_equals && should_add_back_alignment {
@ -34,14 +34,14 @@ pub fn pretty_eval(mut input: String) -> String {
""
},
if should_add_back_backslash { " \\" } else { "" }
)
))
}
#[test]
fn test_character_readdition() {
assert_eq!(pretty_eval(" = 11".into()), " = 11");
assert_eq!(pretty_eval(" &= 11".into()), " & = 11");
assert_eq!(pretty_eval(" & 11".into()), " & 11");
assert_eq!(pretty_eval(" & 11 \\".into()), " & 11 \\");
assert_eq!(pretty_eval(" = 11".into()).unwrap(), " = 11");
assert_eq!(pretty_eval(" &= 11".into()).unwrap(), " & = 11");
assert_eq!(pretty_eval(" & 11".into()).unwrap(), " & 11");
assert_eq!(pretty_eval(" & 11 \\".into()).unwrap(), " & 11 \\");
}
pub fn get_significant_digits(n: &Float) -> usize {

View file

@ -1,12 +1,13 @@
use std::{
env,
io::{self, Read},
process::ExitCode,
};
mod formatting;
mod nodes;
fn main() {
fn main() -> ExitCode {
let input = match env::args().skip(1).rev().reduce(|x, a| a + &x) {
Some(s) => s,
None => {
@ -16,5 +17,14 @@ fn main() {
}
};
println!("{}", formatting::pretty_eval(input));
match formatting::pretty_eval(input) {
Ok(s) => println!("{s}"),
Err(e) => {
eprintln!("{e}");
return ExitCode::FAILURE;
}
}
ExitCode::SUCCESS
}

View file

@ -5,25 +5,35 @@ use rug::{
use crate::nodes::{Function, Node, Nodes, Operator};
#[derive(Clone, Debug, thiserror::Error)]
pub enum Error {
#[error("invalid operand: {0}")]
InvalidOperand(Node),
#[error("missing argument, got: {0}")]
MissingArgument(Node),
#[error("collapse failed, left with: {0}")]
CollapseFailed(Nodes),
}
impl Nodes {
/// Consume the `Nodes`, collapsing them down to a single number
pub fn evaluate(mut self) -> Float {
pub fn evaluate(mut self) -> Result<Float, Error> {
let nodes = &mut self.0;
while let Some(end) = nodes.iter().position(|n| *n == Node::ParenthesisEnd) {
collapse_toplevel(nodes, end);
collapse_toplevel(nodes, end)?;
}
let len = nodes.len();
collapse_toplevel(nodes, len);
collapse_toplevel(nodes, len)?;
match nodes.pop().unwrap() {
Node::Number(n) => n,
_ => panic!("failed to collapse nodes: {nodes:?}"),
Node::Number(n) => Ok(n),
_ => Err(Error::CollapseFailed(self)),
}
}
}
fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) {
fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) -> Result<(), Error> {
let start = nodes[0..end]
.iter()
.rposition(|n| *n == Node::ParenthesisStart)
@ -51,7 +61,7 @@ fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) {
Function::Secant => n.sec_mut(),
Function::CoSecant => n.csc_mut(),
},
n => panic!("function is missing an argument, got: {n:?}"),
n => return Err(Error::MissingArgument(n.clone())),
};
nodes.remove(start + index);
@ -64,12 +74,12 @@ fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) {
{
let lhs = match nodes.remove(start + operator - 1) {
Node::Number(n) => n,
n => panic!("operator has an invalid operand: {n:?}"),
n => return Err(Error::InvalidOperand(n)),
};
end -= 1;
match &mut nodes[start..end][operator] {
Node::Number(n) => n.pow_from(lhs),
n => panic!("operator has an invalid operand: {n:?}"),
n => return Err(Error::InvalidOperand(n.clone())),
};
nodes.remove(start + operator - 1);
@ -82,12 +92,12 @@ fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) {
{
let lhs = match nodes.remove(start + operator - 1) {
Node::Number(n) => n,
n => panic!("operator has an invalid operand: {n:?}"),
n => return Err(Error::InvalidOperand(n)),
};
end -= 1;
match &mut nodes[start..end][operator] {
Node::Number(n) => n.div_from(lhs),
n => panic!("operator has an invalid operand: {n:?}"),
n => return Err(Error::InvalidOperand(n.clone())),
};
nodes.remove(start + operator - 1);
@ -100,12 +110,12 @@ fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) {
{
let lhs = match nodes.remove(start + operator - 1) {
Node::Number(n) => n,
n => panic!("operator has an invalid operand: {n:?}"),
n => return Err(Error::InvalidOperand(n)),
};
end -= 1;
match &mut nodes[start..end][operator] {
Node::Number(n) => n.mul_from(lhs),
n => panic!("operator has an invalid operand: {n:?}"),
n => return Err(Error::InvalidOperand(n.clone())),
};
nodes.remove(start + operator - 1);
@ -118,12 +128,12 @@ fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) {
{
let lhs = match nodes.remove(start + operator - 1) {
Node::Number(n) => n,
n => panic!("operator has an invalid operand: {n:?}"),
n => return Err(Error::InvalidOperand(n)),
};
end -= 1;
match &mut nodes[start..end][operator] {
Node::Number(n) => n.sub_from(lhs),
n => panic!("operator has an invalid operand: {n:?}"),
n => return Err(Error::InvalidOperand(n.clone())),
};
nodes.remove(start + operator - 1);
@ -136,12 +146,12 @@ fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) {
{
let lhs = match nodes.remove(start + operator - 1) {
Node::Number(n) => n,
n => panic!("operator has an invalid operand: {n:?}"),
n => return Err(Error::InvalidOperand(n)),
};
end -= 1;
match &mut nodes[start..end][operator] {
Node::Number(n) => n.add_from(lhs),
n => panic!("operator has an invalid operand: {n:?}"),
n => return Err(Error::InvalidOperand(n.clone())),
};
nodes.remove(start + operator - 1);
@ -152,6 +162,8 @@ fn collapse_toplevel(nodes: &mut Vec<Node>, mut end: usize) {
nodes.remove(end);
nodes.remove(start);
}
Ok(())
}
#[cfg(test)]
@ -169,6 +181,7 @@ fn test() {
Node::Number(Float::with_val(PREC, 1))
])
.evaluate()
.unwrap()
.to_f64(),
1.9092974268256817,
);

View file

@ -2,6 +2,8 @@
//! as well as the logic to parse it from a string (`Node::from_str`),
//! and the logic to evaluate it into a single number (`Node::evaluate`).
use std::fmt::{Debug, Display};
use rug::Float;
mod evaluate;
@ -9,10 +11,18 @@ pub mod from_str;
#[cfg(test)]
mod tests;
#[derive(Clone, Debug, thiserror::Error)]
pub enum Error {
#[error("{0}")]
Parsing(#[from] from_str::Error),
#[error("{0}")]
Evaluation(#[from] evaluate::Error),
}
/// helper function to parse to Nodes and immediately evaluate
pub fn eval(s: &str) -> Result<rug::Float, from_str::Error> {
pub fn eval(s: &str) -> Result<rug::Float, Error> {
use std::str::FromStr;
Nodes::from_str(s).map(|nodes| nodes.evaluate())
Ok(Nodes::from_str(s)?.evaluate()?)
}
pub const PREC: u32 = 512;
@ -24,18 +34,28 @@ impl AsRef<[Node]> for Nodes {
self.0.as_ref()
}
}
impl Display for Nodes {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self.0, f)
}
}
#[derive(Debug, Clone, PartialEq)]
enum Node {
pub enum Node {
Function(Function),
ParenthesisStart,
ParenthesisEnd,
Number(Float),
Operator(Operator),
}
impl Display for Node {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
Debug::fmt(&self, f)
}
}
#[derive(Debug, Default, Copy, Clone, PartialEq)]
enum Operator {
pub enum Operator {
Plus,
Minus,
#[default]
@ -45,7 +65,7 @@ enum Operator {
}
#[derive(Debug, Copy, Clone, PartialEq)]
enum Function {
pub enum Function {
Sqrt,
Sine,

View file

@ -4,7 +4,7 @@ use std::str::FromStr;
use crate::nodes::Nodes;
fn eval(s: &str) -> f64 {
Nodes::from_str(s).unwrap().evaluate().to_f64()
Nodes::from_str(s).unwrap().evaluate().unwrap().to_f64()
}
#[test]