From 5f25c92404d1d4f51b81bc4ac3210b71129b272a Mon Sep 17 00:00:00 2001 From: electria Date: Mon, 28 Sep 2026 12:58:12 -0700 Subject: [PATCH 01/14] fix: plus evaluated before minus --- src/nodes/evaluate.rs | 36 ++++++++++++++++++------------------ src/nodes/tests.rs | 18 ++++++++++++++++++ 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/src/nodes/evaluate.rs b/src/nodes/evaluate.rs index c9984bd..f585097 100644 --- a/src/nodes/evaluate.rs +++ b/src/nodes/evaluate.rs @@ -112,24 +112,6 @@ fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { end -= 1; } - while let Some(operator) = nodes[start..end] - .iter() - .position(|n| *n == Node::Operator(Operator::Plus)) - { - let lhs = match nodes.remove(start + operator - 1) { - Node::Number(n) => n, - n => panic!("operator has an invalid operand: {n:?}"), - }; - end -= 1; - match &mut nodes[start..end][operator] { - Node::Number(n) => n.add_from(lhs), - n => panic!("operator has an invalid operand: {n:?}"), - }; - - nodes.remove(start + operator - 1); - end -= 1; - } - while let Some(operator) = nodes[start..end] .iter() .position(|n| *n == Node::Operator(Operator::Minus)) @@ -148,6 +130,24 @@ fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { end -= 1; } + while let Some(operator) = nodes[start..end] + .iter() + .position(|n| *n == Node::Operator(Operator::Plus)) + { + let lhs = match nodes.remove(start + operator - 1) { + Node::Number(n) => n, + n => panic!("operator has an invalid operand: {n:?}"), + }; + end -= 1; + match &mut nodes[start..end][operator] { + Node::Number(n) => n.add_from(lhs), + n => panic!("operator has an invalid operand: {n:?}"), + }; + + nodes.remove(start + operator - 1); + end -= 1; + } + if nodes.len() > 1 { nodes.remove(end); nodes.remove(start); diff --git a/src/nodes/tests.rs b/src/nodes/tests.rs index 2acaac0..b0bb9ab 100644 --- a/src/nodes/tests.rs +++ b/src/nodes/tests.rs @@ -5,6 +5,24 @@ use rug::Float; use crate::nodes::{Function, Node, Nodes, Operator, PREC}; +#[test] +fn plus_minus_order() { + assert_eq!( + Nodes::from_str("1 - 4 - 5").unwrap().evaluate().to_f64(), + -8. + ); + + assert_eq!( + Nodes::from_str("1 - 4 + 5").unwrap().evaluate().to_f64(), + 2. + ); + + assert_eq!( + Nodes::from_str("1 + 4 - 5").unwrap().evaluate().to_f64(), + 0. + ); +} + #[test] fn e_edge_case() { assert_eq!(Nodes::from_str("1e-1").unwrap().evaluate().to_f64(), 0.1) From c3526bf161d1eb0da20fca661649e30fdba2ce04 Mon Sep 17 00:00:00 2001 From: electria Date: Mon, 28 Sep 2026 13:01:24 -0700 Subject: [PATCH 02/14] refactor: add and use testing helper fn eval --- src/nodes/tests.rs | 52 +++++++++++++--------------------------------- 1 file changed, 15 insertions(+), 37 deletions(-) diff --git a/src/nodes/tests.rs b/src/nodes/tests.rs index b0bb9ab..413d5d1 100644 --- a/src/nodes/tests.rs +++ b/src/nodes/tests.rs @@ -5,62 +5,40 @@ use rug::Float; use crate::nodes::{Function, Node, Nodes, Operator, PREC}; +fn eval(s: &str) -> f64 { + Nodes::from_str(s).unwrap().evaluate().to_f64() +} + #[test] fn plus_minus_order() { - assert_eq!( - Nodes::from_str("1 - 4 - 5").unwrap().evaluate().to_f64(), - -8. - ); - - assert_eq!( - Nodes::from_str("1 - 4 + 5").unwrap().evaluate().to_f64(), - 2. - ); - - assert_eq!( - Nodes::from_str("1 + 4 - 5").unwrap().evaluate().to_f64(), - 0. - ); + assert_eq!(eval("1 - 4 - 5"), -8.); + assert_eq!(eval("1 - 4 + 5"), 2.); + assert_eq!(eval("1 + 4 - 5"), 0.); } #[test] fn e_edge_case() { - assert_eq!(Nodes::from_str("1e-1").unwrap().evaluate().to_f64(), 0.1) + assert_eq!(eval("1e-1"), 0.1) } #[test] fn implicit_mult() { - assert_eq!( - Nodes::from_str("10 sin(10)").unwrap().evaluate().to_f64(), - -5.440211108893698 - ); + assert_eq!(eval("10 sin(10)"), -5.440211108893698); - assert_eq!( - Nodes::from_str("(10)(10)").unwrap().evaluate().to_f64(), - 100., - ) + assert_eq!(eval("(10)(10)"), 100.,) } #[test] fn constants() { - assert_eq!( - Nodes::from_str("c").unwrap().evaluate().to_f64(), - 299792458. - ); - assert_eq!( - Nodes::from_str("A").unwrap().evaluate().to_f64(), - 6.02214076e23 - ); - assert_eq!( - Nodes::from_str("pi").unwrap().evaluate().to_f64(), - f64::consts::PI - ); + assert_eq!(eval("c"), 299792458.); + assert_eq!(eval("A"), 6.02214076e23); + assert_eq!(eval("pi"), f64::consts::PI); } #[test] fn unary_operators() { - assert_eq!(Nodes::from_str("-10").unwrap().evaluate().to_f64(), -10.); - assert_eq!(Nodes::from_str("+10").unwrap().evaluate().to_f64(), 10.); + assert_eq!(eval("-10"), -10.); + assert_eq!(eval("+10"), 10.); } #[test] From 1bb69e7357058b4b905ba030a277cd40805eb28f Mon Sep 17 00:00:00 2001 From: electria Date: Mon, 28 Sep 2026 13:07:29 -0700 Subject: [PATCH 03/14] build: don't duplicate cargo files in the store --- flake.nix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/flake.nix b/flake.nix index 62c5763..f7862ee 100644 --- a/flake.nix +++ b/flake.nix @@ -12,14 +12,14 @@ let pkgs = import inputs.nixpkgs { inherit system; }; - cargoToml = fromTOML (builtins.readFile ./Cargo.toml); + src = ./.; + cargoToml = fromTOML (builtins.readFile "${src}/Cargo.toml"); name = cargoToml.package.name; crate = pkgs.rustPlatform.buildRustPackage { - inherit name; - src = ./.; + inherit name src; - cargoLock.lockFile = ./Cargo.lock; + cargoLock.lockFile = "${src}/Cargo.lock"; nativeBuildInputs = with pkgs; [ m4 From 15534793fd3190be0f9b2a2dd0199073bbfe8243 Mon Sep 17 00:00:00 2001 From: electria Date: Mon, 28 Sep 2026 13:08:32 -0700 Subject: [PATCH 04/14] build: enable thick lto and minimize codegen-units binary size goes from 1224 to 1068 (as reported by `du result/bin/tylc`) --- Cargo.toml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index a6c52cf..5793b00 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,3 +10,7 @@ thiserror = "2.0.18" version = "1.30" default-features = false features = ["std", "float"] + +[profile.release] +lto = true +codegen-units = 1 From 06b6c38f3086b31873ca4399d4a66f0440f249fe Mon Sep 17 00:00:00 2001 From: electria Date: Tue, 29 Sep 2026 16:03:32 -0700 Subject: [PATCH 05/14] feat: ignore and add back trailing backslash --- src/main.rs | 8 +++++--- src/nodes/from_str.rs | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/main.rs b/src/main.rs index 3e9206b..fbb3a72 100644 --- a/src/main.rs +++ b/src/main.rs @@ -34,14 +34,15 @@ pub fn parse_and_calc_humanreadable(mut input: String) -> String { false }; - // no need to remove the character here, - // as we ignore the ampersand in all other logic already + // no need to remove the characters here, + // as we ignore the them in all other logic already let should_add_back_alignment = input.contains('&'); + let should_add_back_backslash = input.contains('\\'); let result = parse_and_calc(&input); format!( - "{0}{result:.1$}", + "{0}{result:.1$}{2}", if should_add_back_equals && should_add_back_alignment { "& = " } else if should_add_back_equals { @@ -52,6 +53,7 @@ pub fn parse_and_calc_humanreadable(mut input: String) -> String { "" }, get_significant_digits(&result), + if should_add_back_backslash { " \\" } else { "" } ) } #[test] diff --git a/src/nodes/from_str.rs b/src/nodes/from_str.rs index 14e4c9a..189f14c 100644 --- a/src/nodes/from_str.rs +++ b/src/nodes/from_str.rs @@ -81,12 +81,12 @@ impl FromStr for Nodes { /// /// We ignore whitespace since it has no semantic significance in typst /// (aside from variables, which are not implemented) - /// and `&` because it is only used for visual alignment. + /// and `&`, `\` because they are only used for visual alignment. /// /// Newlines are also ignored because they're likely accidental; /// like in the case of `echo 1+1 | tylc`. fn is_not_ignored_char(c: &char) -> bool { - !matches!(c, ' ' | '&' | '\n') + !matches!(c, ' ' | '&' | '\n' | '\\') } let mut nodes = Vec::new(); From ca9a522d2b5ad1a5b2c2002e405cedb4fd8444de Mon Sep 17 00:00:00 2001 From: electria Date: Tue, 29 Sep 2026 16:17:32 -0700 Subject: [PATCH 06/14] refactor: drastically simplify should_add_back_equals logic --- src/main.rs | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/src/main.rs b/src/main.rs index fbb3a72..a8f98ac 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,17 +22,12 @@ fn main() { } pub fn parse_and_calc_humanreadable(mut input: String) -> String { - let should_add_back_equals = if let Some((i, _)) = input - .chars() - .enumerate() - .filter(|(_, c)| !matches!(c, ' ' | '&' | '\n')) - .find(|(_, c)| *c == '=') - { - input.remove(i); - true - } else { - false - }; + let should_add_back_equals = input + .find('=') + .inspect(|i| { + input.remove(*i); + }) + .is_some(); // no need to remove the characters here, // as we ignore the them in all other logic already From 9bacccfb71bcddf5f932cccaa265ff5687bf87f9 Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 30 Sep 2026 15:18:15 -0700 Subject: [PATCH 07/14] feat: preserve leading_spaces --- src/main.rs | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/src/main.rs b/src/main.rs index a8f98ac..640fa6e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -22,6 +22,12 @@ fn main() { } pub fn parse_and_calc_humanreadable(mut input: String) -> String { + // no need to remove the characters here, + // as we ignore the them in all other logic already + let should_add_back_alignment = input.contains('&'); + let should_add_back_backslash = input.contains('\\'); + let leading_spaces = input.chars().take_while(|c| *c == ' ').count(); + let should_add_back_equals = input .find('=') .inspect(|i| { @@ -29,15 +35,12 @@ pub fn parse_and_calc_humanreadable(mut input: String) -> String { }) .is_some(); - // no need to remove the characters here, - // as we ignore the them in all other logic already - let should_add_back_alignment = input.contains('&'); - let should_add_back_backslash = input.contains('\\'); - let result = parse_and_calc(&input); + let significant_digits = get_significant_digits(&result); format!( - "{0}{result:.1$}{2}", + "{:leading_spaces$}{}{result:.significant_digits$}{}", + ' ', if should_add_back_equals && should_add_back_alignment { "& = " } else if should_add_back_equals { @@ -47,15 +50,14 @@ pub fn parse_and_calc_humanreadable(mut input: String) -> String { } else { "" }, - get_significant_digits(&result), if should_add_back_backslash { " \\" } else { "" } ) } #[test] fn test_equals_readdition() { - assert_eq!(parse_and_calc_humanreadable(" = 11".into()), "= 11"); - assert_eq!(parse_and_calc_humanreadable(" &= 11".into()), "& = 11"); - assert_eq!(parse_and_calc_humanreadable(" & 11".into()), "& 11"); + assert_eq!(parse_and_calc_humanreadable(" = 11".into()), " = 11"); + assert_eq!(parse_and_calc_humanreadable(" &= 11".into()), " & = 11"); + assert_eq!(parse_and_calc_humanreadable(" & 11".into()), " & 11"); } pub fn parse_and_calc(s: &str) -> Float { From 396deba4e10266c2d73d2e0e7ced4a36a3fb8437 Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 30 Sep 2026 15:30:40 -0700 Subject: [PATCH 08/14] test: add test for backslash re-addition --- src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main.rs b/src/main.rs index 640fa6e..0de49dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -54,10 +54,11 @@ pub fn parse_and_calc_humanreadable(mut input: String) -> String { ) } #[test] -fn test_equals_readdition() { +fn test_character_readdition() { assert_eq!(parse_and_calc_humanreadable(" = 11".into()), " = 11"); assert_eq!(parse_and_calc_humanreadable(" &= 11".into()), " & = 11"); assert_eq!(parse_and_calc_humanreadable(" & 11".into()), " & 11"); + assert_eq!(parse_and_calc_humanreadable(" & 11 \\".into()), " & 11 \\"); } pub fn parse_and_calc(s: &str) -> Float { From 18a777c449bbf4a6e514fed015c902e06515b2f4 Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 30 Sep 2026 15:40:26 -0700 Subject: [PATCH 09/14] refactor: sort unit tests --- src/nodes/evaluate.rs | 19 +++++++++++++++++++ src/nodes/from_str.rs | 15 +++++++++++++++ src/nodes/tests.rs | 36 +----------------------------------- 3 files changed, 35 insertions(+), 35 deletions(-) diff --git a/src/nodes/evaluate.rs b/src/nodes/evaluate.rs index f585097..9c2be17 100644 --- a/src/nodes/evaluate.rs +++ b/src/nodes/evaluate.rs @@ -153,3 +153,22 @@ fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { nodes.remove(start); } } + +use super::PREC; + +#[test] +fn test() { + assert_eq!( + Nodes(vec![ + Node::Function(Function::Sine), + Node::ParenthesisStart, + Node::Number(Float::with_val(PREC, 2)), + Node::ParenthesisEnd, + Node::Operator(Operator::Plus), + Node::Number(Float::with_val(PREC, 1)) + ]) + .evaluate() + .to_f64(), + 1.9092974268256817, + ); +} diff --git a/src/nodes/from_str.rs b/src/nodes/from_str.rs index 189f14c..07f6d4c 100644 --- a/src/nodes/from_str.rs +++ b/src/nodes/from_str.rs @@ -188,3 +188,18 @@ impl FromStr for Nodes { Ok(Self(nodes)) } } + +#[test] +fn test() { + assert_eq!( + Nodes::from_str("sin(2) + 1").unwrap().as_ref(), + &[ + Node::Function(Function::Sine), + Node::ParenthesisStart, + Node::Number(Float::with_val(PREC, 2)), + Node::ParenthesisEnd, + Node::Operator(Operator::Plus), + Node::Number(Float::with_val(PREC, 1)) + ], + ) +} diff --git a/src/nodes/tests.rs b/src/nodes/tests.rs index 413d5d1..439acf4 100644 --- a/src/nodes/tests.rs +++ b/src/nodes/tests.rs @@ -1,9 +1,7 @@ use core::f64; use std::str::FromStr; -use rug::Float; - -use crate::nodes::{Function, Node, Nodes, Operator, PREC}; +use crate::nodes::Nodes; fn eval(s: &str) -> f64 { Nodes::from_str(s).unwrap().evaluate().to_f64() @@ -40,35 +38,3 @@ fn unary_operators() { assert_eq!(eval("-10"), -10.); assert_eq!(eval("+10"), 10.); } - -#[test] -fn test_nodes_to_num() { - assert_eq!( - Nodes(vec![ - Node::Function(Function::Sine), - Node::ParenthesisStart, - Node::Number(Float::with_val(PREC, 2)), - Node::ParenthesisEnd, - Node::Operator(Operator::Plus), - Node::Number(Float::with_val(PREC, 1)) - ]) - .evaluate() - .to_f64(), - 1.9092974268256817, - ); -} - -#[test] -fn test_nodes_from_str() { - assert_eq!( - Nodes::from_str("sin(2) + 1").unwrap().as_ref(), - &[ - Node::Function(Function::Sine), - Node::ParenthesisStart, - Node::Number(Float::with_val(PREC, 2)), - Node::ParenthesisEnd, - Node::Operator(Operator::Plus), - Node::Number(Float::with_val(PREC, 1)) - ], - ) -} From 4363916608ff63fb08be0dece11ca27c91e518d8 Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 30 Sep 2026 17:40:31 -0700 Subject: [PATCH 10/14] refactor: split main.rs to support planned lib.rs --- src/formatting.rs | 100 +++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 101 +--------------------------------------------- 2 files changed, 102 insertions(+), 99 deletions(-) create mode 100644 src/formatting.rs diff --git a/src/formatting.rs b/src/formatting.rs new file mode 100644 index 0000000..9c494c3 --- /dev/null +++ b/src/formatting.rs @@ -0,0 +1,100 @@ +use std::str::FromStr; + +use rug::Float; + +use crate::nodes::Nodes; + +pub fn pretty_eval(mut input: String) -> String { + // no need to remove the characters here, + // as we ignore the them in all other logic already + let should_add_back_alignment = input.contains('&'); + let should_add_back_backslash = input.contains('\\'); + let leading_spaces = input.chars().take_while(|c| *c == ' ').count(); + + let should_add_back_equals = input + .find('=') + .inspect(|i| { + input.remove(*i); + }) + .is_some(); + + let result = Nodes::from_str(&input).unwrap().evaluate(); + let significant_digits = get_significant_digits(&result); + + format!( + "{:leading_spaces$}{}{result:.significant_digits$}{}", + ' ', + if should_add_back_equals && should_add_back_alignment { + "& = " + } else if should_add_back_equals { + "= " + } else if should_add_back_alignment { + "& " + } else { + "" + }, + 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 \\"); +} + +pub fn get_significant_digits(n: &Float) -> usize { + let raw_string: String = format!("{n}").chars().filter(|c| *c != '.').collect(); + + let mut consecutive_zeros = 0; + let mut consecutive_nines = 0; + for (i, c) in raw_string.char_indices() { + if c == '0' { + consecutive_zeros += 1; + } else { + consecutive_zeros = 0; + } + + if c == '9' { + consecutive_nines += 1; + } else { + consecutive_nines = 0; + } + + if consecutive_zeros >= 10 { + return i + 1 - consecutive_zeros; + } else if consecutive_nines >= 10 { + return i + 1 - consecutive_nines; + } + + if !raw_string[i..raw_string.len()].contains(|c| matches!(c, '1'..='9')) { + return i; + } + } + + // all digits are significant + raw_string.len() +} + +#[cfg(test)] +fn eval(s: &str) -> Float { + Nodes::from_str(s).unwrap().evaluate() +} + +#[test] +fn test_get_significant_digits() { + let many_zeros = eval("14.00643 + 1.00784 * 4 + 35.446 + 15.9994 * 4"); + assert_eq!(many_zeros.to_f64(), 117.48139); + let many_nines = eval("12.0096 + 15.9994 * 2"); + assert_eq!(many_nines.to_f64(), 44.0084); + let zero_nine_zero = eval("2+12321(5+123)"); + assert_eq!(zero_nine_zero.to_f64(), 1577090.0); + let test_4 = eval("12.0096 + 1.00784 * 3 + 12.0096 + 1.00784 * 2 + 15.9994 + 1.00784"); + assert_eq!(test_4.to_f64(), 46.06564); + + assert_eq!(get_significant_digits(&many_zeros), 8); + assert_eq!(get_significant_digits(&many_nines), 6); + assert_eq!(get_significant_digits(&zero_nine_zero), 6); + assert_eq!(get_significant_digits(&test_4), 7); +} diff --git a/src/main.rs b/src/main.rs index 0de49dc..e22e595 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,11 +1,9 @@ use std::{ env, io::{self, Read}, - str::FromStr, }; -use rug::Float; - +mod formatting; mod nodes; fn main() { @@ -18,100 +16,5 @@ fn main() { } }; - println!("{}", parse_and_calc_humanreadable(input)); -} - -pub fn parse_and_calc_humanreadable(mut input: String) -> String { - // no need to remove the characters here, - // as we ignore the them in all other logic already - let should_add_back_alignment = input.contains('&'); - let should_add_back_backslash = input.contains('\\'); - let leading_spaces = input.chars().take_while(|c| *c == ' ').count(); - - let should_add_back_equals = input - .find('=') - .inspect(|i| { - input.remove(*i); - }) - .is_some(); - - let result = parse_and_calc(&input); - let significant_digits = get_significant_digits(&result); - - format!( - "{:leading_spaces$}{}{result:.significant_digits$}{}", - ' ', - if should_add_back_equals && should_add_back_alignment { - "& = " - } else if should_add_back_equals { - "= " - } else if should_add_back_alignment { - "& " - } else { - "" - }, - if should_add_back_backslash { " \\" } else { "" } - ) -} -#[test] -fn test_character_readdition() { - assert_eq!(parse_and_calc_humanreadable(" = 11".into()), " = 11"); - assert_eq!(parse_and_calc_humanreadable(" &= 11".into()), " & = 11"); - assert_eq!(parse_and_calc_humanreadable(" & 11".into()), " & 11"); - assert_eq!(parse_and_calc_humanreadable(" & 11 \\".into()), " & 11 \\"); -} - -pub fn parse_and_calc(s: &str) -> Float { - nodes::Nodes::from_str(s).unwrap().evaluate() -} - -pub fn get_significant_digits(n: &Float) -> usize { - let raw_string: String = format!("{n}").chars().filter(|c| *c != '.').collect(); - - let mut consecutive_zeros = 0; - let mut consecutive_nines = 0; - for (i, c) in raw_string.char_indices() { - if c == '0' { - consecutive_zeros += 1; - } else { - consecutive_zeros = 0; - } - - if c == '9' { - consecutive_nines += 1; - } else { - consecutive_nines = 0; - } - - if consecutive_zeros >= 10 { - return i + 1 - consecutive_zeros; - } else if consecutive_nines >= 10 { - return i + 1 - consecutive_nines; - } - - if !raw_string[i..raw_string.len()].contains(|c| matches!(c, '1'..='9')) { - return i; - } - } - - // all digits are significant - raw_string.len() -} - -#[test] -fn test_get_significant_digits() { - let many_zeros = parse_and_calc("14.00643 + 1.00784 * 4 + 35.446 + 15.9994 * 4"); - assert_eq!(many_zeros.to_f64(), 117.48139); - let many_nines = parse_and_calc("12.0096 + 15.9994 * 2"); - assert_eq!(many_nines.to_f64(), 44.0084); - let zero_nine_zero = parse_and_calc("2+12321(5+123)"); - assert_eq!(zero_nine_zero.to_f64(), 1577090.0); - let test_4 = - parse_and_calc("12.0096 + 1.00784 * 3 + 12.0096 + 1.00784 * 2 + 15.9994 + 1.00784"); - assert_eq!(test_4.to_f64(), 46.06564); - - assert_eq!(get_significant_digits(&many_zeros), 8); - assert_eq!(get_significant_digits(&many_nines), 6); - assert_eq!(get_significant_digits(&zero_nine_zero), 6); - assert_eq!(get_significant_digits(&test_4), 7); + println!("{}", formatting::pretty_eval(input)); } From ae9e6604e857e18a6e4d274bc3403a76af34235e Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 30 Sep 2026 18:18:35 -0700 Subject: [PATCH 11/14] refactor: fix unused lint --- src/nodes/evaluate.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/nodes/evaluate.rs b/src/nodes/evaluate.rs index 9c2be17..ceb768a 100644 --- a/src/nodes/evaluate.rs +++ b/src/nodes/evaluate.rs @@ -154,6 +154,7 @@ fn collapse_toplevel(nodes: &mut Vec, mut end: usize) { } } +#[cfg(test)] use super::PREC; #[test] From d2368adc8f0dd7d6a22bdcd51d72ed99afc7f241 Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 30 Sep 2026 18:25:46 -0700 Subject: [PATCH 12/14] feat: create lib.rs API --- src/formatting.rs | 2 +- src/lib.rs | 4 ++++ src/nodes/mod.rs | 8 +++++++- 3 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 src/lib.rs diff --git a/src/formatting.rs b/src/formatting.rs index 9c494c3..b439616 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -79,7 +79,7 @@ pub fn get_significant_digits(n: &Float) -> usize { #[cfg(test)] fn eval(s: &str) -> Float { - Nodes::from_str(s).unwrap().evaluate() + crate::nodes::eval(s).unwrap() } #[test] diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..71e5c48 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,4 @@ +pub mod formatting; +pub mod nodes; + +pub use nodes::eval; diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index f46ad3b..1f8f81b 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -5,10 +5,16 @@ use rug::Float; mod evaluate; -mod from_str; +pub mod from_str; #[cfg(test)] mod tests; +/// helper function to parse to Nodes and immediately evaluate +pub fn eval(s: &str) -> Result { + use std::str::FromStr; + Nodes::from_str(s).map(|nodes| nodes.evaluate()) +} + pub const PREC: u32 = 512; #[derive(Debug, Default, Clone, PartialEq)] From 35da9a0d40b85e3a71eb1ceefe38c5fa4a87677c Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 30 Sep 2026 19:39:34 -0700 Subject: [PATCH 13/14] 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 --- src/formatting.rs | 18 ++++++++--------- src/main.rs | 14 +++++++++++-- src/nodes/evaluate.rs | 47 +++++++++++++++++++++++++++---------------- src/nodes/mod.rs | 30 ++++++++++++++++++++++----- src/nodes/tests.rs | 2 +- 5 files changed, 77 insertions(+), 34 deletions(-) diff --git a/src/formatting.rs b/src/formatting.rs index b439616..e20e378 100644 --- a/src/formatting.rs +++ b/src/formatting.rs @@ -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 { // 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 { diff --git a/src/main.rs b/src/main.rs index e22e595..32f04a3 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 } diff --git a/src/nodes/evaluate.rs b/src/nodes/evaluate.rs index ceb768a..27d6907 100644 --- a/src/nodes/evaluate.rs +++ b/src/nodes/evaluate.rs @@ -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 { 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, mut end: usize) { +fn collapse_toplevel(nodes: &mut Vec, 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, 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, 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, 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, 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, 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, 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, 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, ); diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index 1f8f81b..32daaff 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -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 { +pub fn eval(s: &str) -> Result { 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, diff --git a/src/nodes/tests.rs b/src/nodes/tests.rs index 439acf4..1d40f50 100644 --- a/src/nodes/tests.rs +++ b/src/nodes/tests.rs @@ -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] From 2deabb148e7085eb3c9baabd9aa875e062e1a1ff Mon Sep 17 00:00:00 2001 From: electria Date: Wed, 30 Sep 2026 19:48:14 -0700 Subject: [PATCH 14/14] refactor: rename modules to nicer names --- src/nodes/{evaluate.rs => eval.rs} | 0 src/nodes/mod.rs | 8 ++++---- src/nodes/{from_str.rs => parse.rs} | 0 3 files changed, 4 insertions(+), 4 deletions(-) rename src/nodes/{evaluate.rs => eval.rs} (100%) rename src/nodes/{from_str.rs => parse.rs} (100%) diff --git a/src/nodes/evaluate.rs b/src/nodes/eval.rs similarity index 100% rename from src/nodes/evaluate.rs rename to src/nodes/eval.rs diff --git a/src/nodes/mod.rs b/src/nodes/mod.rs index 32daaff..f435db4 100644 --- a/src/nodes/mod.rs +++ b/src/nodes/mod.rs @@ -6,17 +6,17 @@ use std::fmt::{Debug, Display}; use rug::Float; -mod evaluate; -pub mod from_str; +mod eval; +pub mod parse; #[cfg(test)] mod tests; #[derive(Clone, Debug, thiserror::Error)] pub enum Error { #[error("{0}")] - Parsing(#[from] from_str::Error), + Parsing(#[from] parse::Error), #[error("{0}")] - Evaluation(#[from] evaluate::Error), + Evaluation(#[from] eval::Error), } /// helper function to parse to Nodes and immediately evaluate diff --git a/src/nodes/from_str.rs b/src/nodes/parse.rs similarity index 100% rename from src/nodes/from_str.rs rename to src/nodes/parse.rs