diff --git a/Cargo.lock b/Cargo.lock index 591a1f5..e0a132f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/Cargo.toml b/Cargo.toml index 9c0beee..f0addc4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/src/main.rs b/src/main.rs index 20266db..fffa559 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,4 @@ +pub mod math; pub mod modules; pub mod widget; diff --git a/src/math/eval.rs b/src/math/eval.rs new file mode 100644 index 0000000..0c7b45e --- /dev/null +++ b/src/math/eval.rs @@ -0,0 +1,65 @@ +use thiserror::Error; + +use crate::math::{BinMathOp, MathExpr, Number, UnMathOp}; + +pub fn eval_math_expr(expr: MathExpr) -> Result { + 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 { + 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 + ); + } +} diff --git a/src/math/mod.rs b/src/math/mod.rs new file mode 100644 index 0000000..3ed1d69 --- /dev/null +++ b/src/math/mod.rs @@ -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>), + BinOp(BinMathOp, Box>, Box>), +} + +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, + } + } +} diff --git a/src/math/parser.rs b/src/math/parser.rs new file mode 100644 index 0000000..14a2104 --- /dev/null +++ b/src/math/parser.rs @@ -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 + +// Grammer for LR parser +// <> is a terminal +// +// := '-' +// := '+' | '-' +// := '/' | '*' +// := '**' | '^' +// := 'sqrt' | 'sin' | 'cos' | etc +// +// +// F := +// | +// | +// | '(' binOp1 ')' +// | F +// | '(' binOp1 ')' +// +// +// binOp3 := +// | binOp3 F +// | F +// +// binOp2 := +// | binOp2 binOp3 +// | binOp2 '(' expr ')' [Implicit multiplication] // TODO: +// | binOp3 +// +// binOp1 := +// | binOp1 binOp2 +// | binOp2 +// +// goal := binOp1 + +pub fn parse_expr_from_tokens(tokens: Vec) -> Result { + let mut tokens = tokens.into_iter().peekable(); + parse_expr(&mut tokens) +} + +fn parse_primary<'a>( + tokens: &mut Peekable>>, +) -> Result, 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>>, +) -> Result, MathParserError<'a>> { + parse_expr_prime(parse_primary(tokens)?, tokens, 0) +} + +fn parse_expr_prime<'a>( + mut lhs: MathExpr<'a>, + tokens: &mut Peekable>>, + min_precedence: u8, +) -> Result, 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 { + 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.)) + )) + ) + ); + } +} diff --git a/src/math/tokenizer.rs b/src/math/tokenizer.rs new file mode 100644 index 0000000..b4fd039 --- /dev/null +++ b/src/math/tokenizer.rs @@ -0,0 +1,125 @@ +use std::iter::Peekable; + +use thiserror::Error; + +use crate::math::{MathToken, Number}; + +pub fn tokenize(input: &str) -> Result, 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>, + 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>, + 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] + ); + } +} diff --git a/src/modules/desktop.rs b/src/modules/desktop.rs index e6049de..54fe518 100644 --- a/src/modules/desktop.rs +++ b/src/modules/desktop.rs @@ -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::{