Math module

Yes this is a full math parser and interpreter
This commit is contained in:
kalle 2025-06-25 12:08:38 +02:00
parent 6774bef64c
commit 93131165ee
8 changed files with 536 additions and 4 deletions

21
Cargo.lock generated
View file

@ -20,6 +20,7 @@ dependencies = [
"nucleo-matcher",
"regex",
"shlex",
"thiserror",
]
[[package]]
@ -698,6 +699,26 @@ version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "toml"
version = "0.8.23"

View file

@ -9,3 +9,4 @@ gtk4-layer-shell = "0.5.0"
nucleo-matcher = "0.3.1"
regex = "1.11.1"
shlex = "1.3.0"
thiserror = "2.0.12"

View file

@ -1,3 +1,4 @@
pub mod math;
pub mod modules;
pub mod widget;

65
src/math/eval.rs Normal file
View file

@ -0,0 +1,65 @@
use thiserror::Error;
use crate::math::{BinMathOp, MathExpr, Number, UnMathOp};
pub fn eval_math_expr(expr: MathExpr) -> Result<Number, MathEvalError> {
match expr {
MathExpr::Number(n) => Ok(n),
// TODO: Non floating point arithmetic please
MathExpr::Sym("pi") => Ok(std::f64::consts::PI),
MathExpr::Sym("e") => Ok(std::f64::consts::E),
MathExpr::Sym(s) => Err(MathEvalError::UnkownSymbol(s)),
MathExpr::UnOp(UnMathOp::Minus, expr) => Ok(-eval_math_expr(*expr)?),
MathExpr::BinOp(BinMathOp::Add, lhs, rhs) => {
Ok(eval_math_expr(*lhs)? + eval_math_expr(*rhs)?)
}
MathExpr::BinOp(BinMathOp::Sub, lhs, rhs) => {
Ok(eval_math_expr(*lhs)? - eval_math_expr(*rhs)?)
}
MathExpr::BinOp(BinMathOp::Mult, lhs, rhs) => {
Ok(eval_math_expr(*lhs)? * eval_math_expr(*rhs)?)
}
MathExpr::BinOp(BinMathOp::Div, lhs, rhs) => {
Ok(eval_math_expr(*lhs)? / eval_math_expr(*rhs)?)
}
MathExpr::BinOp(BinMathOp::Pow, lhs, rhs) => {
Ok(eval_math_expr(*lhs)?.powf(eval_math_expr(*rhs)?))
}
}
}
#[derive(Debug, Error)]
pub enum MathEvalError<'a> {
#[error("Encountered unkown symbol while evaluating: {0}")]
UnkownSymbol(&'a str),
}
#[cfg(test)]
mod test {
use crate::math::{
Number,
eval::{MathEvalError, eval_math_expr},
parser::parse_expr_from_tokens,
tokenizer::tokenize,
};
fn eval_str(input: &str) -> Result<Number, MathEvalError> {
eval_math_expr(parse_expr_from_tokens(tokenize(input).unwrap()).unwrap())
}
#[test]
fn basic_expressions() {
assert_eq!(eval_str("1+1").unwrap(), 2.0);
assert_eq!(eval_str("(1+1) * 6").unwrap(), 12.0);
assert_eq!(eval_str("1/2").unwrap(), 0.5);
assert_eq!(eval_str("2+2*3").unwrap(), 8.0);
assert_eq!(eval_str("2+2*3**2").unwrap(), 20.0);
assert_eq!(eval_str("pi").unwrap(), std::f64::consts::PI);
assert_eq!(eval_str("e").unwrap(), std::f64::consts::E);
assert_eq!(eval_str("2*pi").unwrap(), 2.0 * std::f64::consts::PI);
assert_eq!(
eval_str("pi * e").unwrap(),
std::f64::consts::PI * std::f64::consts::E
);
}
}

112
src/math/mod.rs Normal file
View file

@ -0,0 +1,112 @@
// TODO: This should be its own crate
use std::fmt::Display;
pub mod eval;
pub mod parser;
pub mod tokenizer;
// TODO: Non floating point arithmetic please
pub type Number = f64;
#[derive(Debug, PartialEq, PartialOrd, Clone)]
pub enum MathExpr<'a> {
Number(Number),
Sym(&'a str),
UnOp(UnMathOp, Box<MathExpr<'a>>),
BinOp(BinMathOp, Box<MathExpr<'a>>, Box<MathExpr<'a>>),
}
impl<'a> Display for MathExpr<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MathExpr::Number(n) => write!(f, "{n}"),
MathExpr::Sym(s) => write!(f, "{s}"),
MathExpr::UnOp(op, expr) => write!(f, "{op}{expr}"),
MathExpr::BinOp(op, lhs, rhs) => write!(f, "({lhs}{op}{rhs})"),
}
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
pub enum UnMathOp {
Minus,
}
impl Display for UnMathOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
UnMathOp::Minus => f.write_str("-"),
}
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone)]
pub enum BinMathOp {
Add,
Sub,
Mult,
Div,
// TODO: Integer division
// IntegerDiv,
Pow,
}
impl Display for BinMathOp {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
BinMathOp::Add => f.write_str("+"),
BinMathOp::Sub => f.write_str("-"),
BinMathOp::Mult => f.write_str("*"),
BinMathOp::Div => f.write_str("/"),
BinMathOp::Pow => f.write_str("^"),
}
}
}
#[derive(Debug, PartialEq, PartialOrd, Clone, Copy)]
pub enum MathToken<'a> {
Number(Number),
Sym(&'a str),
LParen,
RParen,
Plus,
Minus,
Slash,
Star,
DoubleStar,
Caret,
}
impl<'a> Display for MathToken<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
MathToken::Number(n) => write!(f, "{n}"),
MathToken::Sym(s) => write!(f, "{s}"),
MathToken::LParen => f.write_str("("),
MathToken::RParen => f.write_str(")"),
MathToken::Plus => f.write_str("+"),
MathToken::Minus => f.write_str("-"),
MathToken::Slash => f.write_str("/"),
MathToken::Star => f.write_str("*"),
MathToken::DoubleStar => f.write_str("**"),
MathToken::Caret => f.write_str("^"),
}
}
}
impl<'a> MathToken<'a> {
pub fn precedence(&self) -> u8 {
match self {
MathToken::Number(_) => 0,
MathToken::Sym(_) => 0,
MathToken::LParen => 0,
MathToken::RParen => 0,
MathToken::Plus => 1,
MathToken::Minus => 1,
MathToken::Slash => 2,
MathToken::Star => 2,
MathToken::DoubleStar => 3,
MathToken::Caret => 3,
}
}
}

210
src/math/parser.rs Normal file
View file

@ -0,0 +1,210 @@
use std::iter::Peekable;
use thiserror::Error;
use crate::math::{BinMathOp, MathExpr, MathToken, UnMathOp};
// New thing on stack -> [what to consume, other thing to consume]
// Done -> BinOp <eof>
// Grammer for LR parser
// <> is a terminal
//
// <unOp> := '-'
// <binOp1> := '+' | '-'
// <binOp2> := '/' | '*'
// <binOp3> := '**' | '^'
// <function> := 'sqrt' | 'sin' | 'cos' | etc
//
//
// F :=
// | <number>
// | <sym>
// | '(' binOp1 ')'
// | <unOp> F
// | <function> '(' binOp1 ')'
//
//
// binOp3 :=
// | binOp3 <binOp3> F
// | F
//
// binOp2 :=
// | binOp2 <binOp2> binOp3
// | binOp2 '(' expr ')' [Implicit multiplication] // TODO:
// | binOp3
//
// binOp1 :=
// | binOp1 <binOp1> binOp2
// | binOp2
//
// goal := binOp1 <eof>
pub fn parse_expr_from_tokens(tokens: Vec<MathToken>) -> Result<MathExpr, MathParserError> {
let mut tokens = tokens.into_iter().peekable();
parse_expr(&mut tokens)
}
fn parse_primary<'a>(
tokens: &mut Peekable<impl Iterator<Item = MathToken<'a>>>,
) -> Result<MathExpr<'a>, MathParserError<'a>> {
match tokens.next() {
Some(MathToken::Number(n)) => Ok(MathExpr::Number(n)),
Some(MathToken::Sym(s)) => Ok(MathExpr::Sym(s)),
Some(MathToken::LParen) => {
let expr = parse_expr(tokens)?;
match tokens.next() {
Some(MathToken::RParen) => Ok(expr),
Some(t) => Err(MathParserError::UnexpectedToken(t)),
None => Err(MathParserError::EndOfExpression),
}
}
Some(MathToken::Minus) => Ok(MathExpr::UnOp(
UnMathOp::Minus,
Box::new(parse_expr(tokens)?),
)),
// TODO: Function calls such as sqrt, sin, etc
Some(t) => Err(MathParserError::UnexpectedToken(t)),
None => Err(MathParserError::EndOfExpression),
}
}
fn parse_expr<'a>(
tokens: &mut Peekable<impl Iterator<Item = MathToken<'a>>>,
) -> Result<MathExpr<'a>, MathParserError<'a>> {
parse_expr_prime(parse_primary(tokens)?, tokens, 0)
}
fn parse_expr_prime<'a>(
mut lhs: MathExpr<'a>,
tokens: &mut Peekable<impl Iterator<Item = MathToken<'a>>>,
min_precedence: u8,
) -> Result<MathExpr<'a>, MathParserError<'a>> {
loop {
match tokens.peek() {
// If the token has zero precedence it is not an operator and should stop expression
// parsing.
Some(t) if t.precedence() == 0 => return Ok(lhs),
// If the token has a precedence lower then the minimum we don't parse it right now.
Some(t) if t.precedence() < min_precedence => return Ok(lhs),
// If the token has a precedence of at least min_precedence we parse it.
Some(token) => {
let op_precedence = token.precedence();
let op = parse_bin_op_from_token(*token)?;
// Advance iterator
let _ = tokens.next();
let mut rhs = parse_primary(tokens)?;
loop {
match tokens.peek() {
Some(t) if t.precedence() > op_precedence => {
rhs = parse_expr_prime(rhs, tokens, op_precedence + 1)?;
}
_ => break,
}
}
lhs = MathExpr::BinOp(op, Box::new(lhs), Box::new(rhs));
}
None => return Ok(lhs),
}
}
}
fn parse_bin_op_from_token(token: MathToken) -> Result<BinMathOp, MathParserError> {
match token {
MathToken::Plus => Ok(BinMathOp::Add),
MathToken::Minus => Ok(BinMathOp::Sub),
MathToken::Slash => Ok(BinMathOp::Div),
MathToken::Star => Ok(BinMathOp::Mult),
MathToken::DoubleStar => Ok(BinMathOp::Pow),
MathToken::Caret => Ok(BinMathOp::Pow),
t => Err(MathParserError::UnexpectedToken(t)),
}
}
#[derive(Debug, Error, PartialEq)]
pub enum MathParserError<'a> {
#[error("Unexpected token {0}")]
UnexpectedToken(MathToken<'a>),
#[error("Reached the end of the expression")]
EndOfExpression,
}
#[cfg(test)]
mod test {
use crate::math::parser::{MathParserError, parse_expr_from_tokens};
use crate::math::tokenizer::tokenize;
use crate::math::{BinMathOp, MathExpr, MathToken::*};
#[test]
fn basic_examples() {
assert_eq!(
parse_expr_from_tokens(vec![]).err().unwrap(),
MathParserError::EndOfExpression
);
assert_eq!(
parse_expr_from_tokens(vec![Number(1.)]).unwrap(),
MathExpr::Number(1.)
);
assert_eq!(
parse_expr_from_tokens(vec![Number(1.), Plus, Number(1.)]).unwrap(),
MathExpr::BinOp(
BinMathOp::Add,
Box::new(MathExpr::Number(1.)),
Box::new(MathExpr::Number(1.))
)
);
assert_eq!(
parse_expr_from_tokens(vec![LParen, Number(1.), Plus, Number(1.), RParen]).unwrap(),
MathExpr::BinOp(
BinMathOp::Add,
Box::new(MathExpr::Number(1.)),
Box::new(MathExpr::Number(1.))
)
);
}
#[test]
fn advanced_examples() {
assert_eq!(
parse_expr_from_tokens(tokenize("1+2*3-5**10").unwrap()).unwrap(),
MathExpr::BinOp(
BinMathOp::Sub,
Box::new(MathExpr::BinOp(
BinMathOp::Add,
Box::new(MathExpr::Number(1.)),
Box::new(MathExpr::BinOp(
BinMathOp::Mult,
Box::new(MathExpr::Number(2.)),
Box::new(MathExpr::Number(3.))
))
)),
Box::new(MathExpr::BinOp(
BinMathOp::Pow,
Box::new(MathExpr::Number(5.)),
Box::new(MathExpr::Number(10.))
))
)
);
assert_eq!(
parse_expr_from_tokens(tokenize("(1+(2*3))-(5**10)").unwrap()).unwrap(),
MathExpr::BinOp(
BinMathOp::Sub,
Box::new(MathExpr::BinOp(
BinMathOp::Add,
Box::new(MathExpr::Number(1.)),
Box::new(MathExpr::BinOp(
BinMathOp::Mult,
Box::new(MathExpr::Number(2.)),
Box::new(MathExpr::Number(3.))
))
)),
Box::new(MathExpr::BinOp(
BinMathOp::Pow,
Box::new(MathExpr::Number(5.)),
Box::new(MathExpr::Number(10.))
))
)
);
}
}

125
src/math/tokenizer.rs Normal file
View file

@ -0,0 +1,125 @@
use std::iter::Peekable;
use thiserror::Error;
use crate::math::{MathToken, Number};
pub fn tokenize(input: &str) -> Result<Vec<MathToken>, MathTokenizerError> {
let mut tokens = Vec::new();
let mut chars = input.char_indices().peekable();
loop {
match chars.next() {
Some((_, ' ')) => { /* Spaces can be ignored in the general case. */ }
Some((_, '(')) => tokens.push(MathToken::LParen),
Some((_, ')')) => tokens.push(MathToken::RParen),
Some((_, '+')) => tokens.push(MathToken::Plus),
Some((_, '-')) => tokens.push(MathToken::Minus),
// TODO: Integer division with double //
Some((_, '/')) => tokens.push(MathToken::Slash),
Some((_, '*')) => match chars.peek() {
Some((_, '*')) => {
chars.next();
tokens.push(MathToken::DoubleStar)
}
_ => tokens.push(MathToken::Star),
},
Some((_, '^')) => tokens.push(MathToken::Caret),
Some((start_idx, c)) if c.is_alphabetic() => {
// This is an yet unknown symbol, call the helper to find its end and return the
// end index. Then take a slice of that symbol and store it.
let end_idx = tokenize_sym(&mut chars, input.len());
tokens.push(MathToken::Sym(&input[start_idx..end_idx]));
}
Some((start_idx, c)) if c.is_ascii_digit() || c == '.' => {
let end_idx = tokenize_number(c == '.', &mut chars, input.len());
let number: Number = input[start_idx..end_idx]
.parse()
.map_err(|_| MathTokenizerError::InvalidNumber(&input[start_idx..end_idx]))?;
tokens.push(MathToken::Number(number));
}
Some((_, c)) => {
return Err(MathTokenizerError::InvalidChar(c));
}
// No more chars, so we are done
None => break,
}
}
Ok(tokens)
}
fn tokenize_sym(
chars: &mut Peekable<impl Iterator<Item = (usize, char)>>,
input_end_idx: usize,
) -> usize {
loop {
match chars.peek() {
// Alphabetic chars can be symbols, so continue on.
Some((_, c)) if c.is_alphabetic() => {
chars.next();
}
// Anything other then alphabetic chars means end of symbol
Some((end_idx, _)) => return *end_idx,
// If we reach the end of the string, that is the end of the symbol
None => return input_end_idx,
}
}
}
fn tokenize_number<'a>(
first_is_dot: bool,
chars: &mut Peekable<impl Iterator<Item = (usize, char)>>,
input_end_idx: usize,
) -> usize {
let mut has_seen_dot = first_is_dot;
loop {
match chars.peek() {
Some((_, '.')) if !has_seen_dot => {
has_seen_dot = true;
chars.next();
}
Some((_, c)) if c.is_ascii_digit() => {
chars.next();
}
Some((end_idx, _)) => return *end_idx,
None => return input_end_idx,
}
}
}
#[derive(Debug, Error)]
pub enum MathTokenizerError<'a> {
#[error("Invalid character '{0}' in input")]
InvalidChar(char),
#[error("Invalid number \"{0}\" in input")]
InvalidNumber(&'a str),
}
#[cfg(test)]
mod test {
use crate::math::MathToken::*;
use crate::math::tokenizer::tokenize;
#[test]
fn basic_examples() {
assert_eq!(tokenize("").unwrap(), vec![]);
assert_eq!(tokenize("1").unwrap(), vec![Number(1.)]);
assert_eq!(tokenize("1.0").unwrap(), vec![Number(1.)]);
assert_eq!(tokenize(".1").unwrap(), vec![Number(0.1)]);
assert_eq!(tokenize("1+1").unwrap(), vec![Number(1.), Plus, Number(1.)]);
assert_eq!(
tokenize("1 + 1").unwrap(),
vec![Number(1.), Plus, Number(1.)]
);
assert_eq!(
tokenize(" 1 + 1 ").unwrap(),
vec![Number(1.), Plus, Number(1.)]
);
assert_eq!(
tokenize("(1+1)").unwrap(),
vec![LParen, Number(1.), Plus, Number(1.), RParen]
);
}
}

View file

@ -4,10 +4,7 @@ use std::{
};
use gtk::{
gio::{
AppInfo, DesktopAppInfo, Icon,
prelude::{AppInfoExt, DesktopAppInfoExtManual},
},
gio::{AppInfo, DesktopAppInfo, Icon, prelude::AppInfoExt},
prelude::BoxExt,
};
use nucleo_matcher::{