diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/commands/calc.pest | 79 | ||||
| -rw-r--r-- | src/commands/mod.rs | 5 | ||||
| -rw-r--r-- | src/commands/roll.rs | 422 | ||||
| -rw-r--r-- | src/main.rs | 3 |
4 files changed, 210 insertions, 299 deletions
diff --git a/src/commands/calc.pest b/src/commands/calc.pest new file mode 100644 index 0000000..07eeddb --- /dev/null +++ b/src/commands/calc.pest @@ -0,0 +1,79 @@ +num = { + hex + | oct + | binary + | float +} + +float = @{ int ~ ( "." ~ ASCII_DIGIT*)? ~ (^"e" ~ int)? } + int = { "-"? ~ ASCII_DIGIT+ } + +hex = @{ "0x" ~ ASCII_HEX_DIGIT+ } +oct = @{ "0o" ~ ASCII_OCT_DIGIT+ } +binary = @{ "0b" ~ ASCII_BIN_DIGIT+ } + +infix = _{ add | sub | mul | div | modulo } + add = { "+" } + sub = { "-" } + modulo = { "%" | "mod" } + mul = { "*" } + div = { "/" } + +tight_infix = _{ dice | pow } + dice = { "d" } + pow = { "^" } + +trig = _{ sin | cos | tan | asin | acos | atan } + sin = { "sin" } + cos = { "cos" } + tan = { "tan" } + asin = { "asin" } + acos = { "acos" } + atan = { "atan" } + +htrig = _{ sinh | cosh | tanh | asinh | acosh | atanh } + sinh = { "sinh" } + cosh = { "cosh" } + tanh = { "tanh" } + asinh = { "asinh" } + acosh = { "acosh" } + atanh = { "atanh" } + +unary_prefix = _{ log | sqrt | sgn | htrig | trig | exp | abs | ceil | floor | round } + log = { "log" | "ln" } + sqrt = { "sqrt" } + sgn = { "sgn" } + exp = { "exp" } + abs = { "abs" } + ceil = { "ceil" } + floor = { "floor" } + round = { "round" } + +binary_prefix = _{ min | max | atan2 } + min = { "min" } + max = { "max" } + atan2 = { "atan2" } + +suffix = _{ factorial } + factorial = { "!" } + +term = _{ num | "(" ~ expr ~ ")" } + +suffix_expr = { term ~ suffix } +unary_expr = ${ unary_prefix ~ ws+ ~ outfix_expr } +binary_expr = ${ binary_prefix ~ ws+ ~ outfix_expr ~ ws+ ~ outfix_expr } + +tight = _{ (suffix_expr | term) ~ (tight_infix ~ tight)* } + +expr = { outfix_expr ~ (infix ~ outfix_expr)* } + +outfix_expr = _{ + tight | + binary_expr | + unary_expr +} + +calc = _{ SOI ~ expr ~ EOI } + +ws = _{ " " | "\t" | "\n" } +WHITESPACE = _{ ws } diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 1582977..6596a32 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -60,11 +60,6 @@ pub fn register_commands(f: StandardFramework) -> StandardFramework { .desc("simulate rolling dice") .guild_only(true) .exec(roll::roll)) - .command("debug_expr", |c| c - .desc("debug calculator expression") - .owners_only(true) - .exec(roll::debug_expr) - ) .unrecognised_command(|ctx, msg, unrec| { let url = match msg.content.split_whitespace().skip(1).next() { Some(x) if x.starts_with("http") => x, diff --git a/src/commands/roll.rs b/src/commands/roll.rs index bdb729e..b447692 100644 --- a/src/commands/roll.rs +++ b/src/commands/roll.rs @@ -1,10 +1,3 @@ -use failure::Error; -use itertools::Itertools; -use nom::{ - self, - double, - types::CompleteStr, -}; use rand::prelude::*; use serenity::{ framework::standard::Args, @@ -18,334 +11,177 @@ use crate::{ Result, }; -#[derive(Clone, Debug, PartialEq)] -enum CalcExpr { - Binary(BinOp, Box<CalcExpr>, Box<CalcExpr>), - Unary(UnaryOp, Box<CalcExpr>), - Term(f64), -} - -#[derive(Clone, Debug, PartialEq, Fail)] -enum CalcParseError { - #[fail(display = "couldn't consume entire expression. parsed: {:?}, remaining: '{}'.", parsed, remaining)] - NotReadToEnd { - parsed: Box<CalcExpr>, - remaining: String, - }, - #[fail(display = "nom error: {}", _0)] - Nom(String), -} +#[derive(Parser)] +#[grammar = "commands/calc.pest"] +struct Calc; -impl CalcExpr { - pub fn parse<S: AsRef<str>>(input: S) -> Result<Box<Self>> { - parse_expr(CompleteStr(input.as_ref())) - .map_err(|e| CalcParseError::Nom(format!("{}", e))) - .and_then(|(s, res)| { - if s.len() != 0 { - Err(CalcParseError::NotReadToEnd { - parsed: res, - remaining: s.as_ref().to_owned(), - }) - } else { - Ok(res) - } - }) - .map_err(Error::from) - } +impl Calc { + fn eval<S: AsRef<str>>(s: S) -> Result<f64> { + use pest::{ + Parser, + prec_climber::PrecClimber, + iterators::{Pair, Pairs}, + }; - pub fn pretty(&self) -> String { - format!("```\n{}\n```", self.pretty_helper(0)) - } + use self::Rule::*; - fn pretty_helper(&self, depth: usize) -> String { - match self { - CalcExpr::Binary(op, e1, e2) => { - let lines = vec! { - format!("{}{:?} ({}) {{", "\t".repeat(depth), op, self.compute()), - e1.pretty_helper(depth + 1), - e2.pretty_helper(depth + 1), - "\t".repeat(depth) + "}", + lazy_static! { + static ref CLIMBER: PrecClimber<self::Rule> = { + use pest::prec_climber::{ + Operator, + Assoc::*, }; - lines.into_iter().join("\n") - }, - CalcExpr::Unary(op, e) => { - let lines = vec! { - format!("{}{:?} ({}) {{", "\t".repeat(depth), op, self.compute()), - e.pretty_helper(depth + 1), - "\t".repeat(depth) + "}", - }; - - lines.into_iter().join("\n") - }, - CalcExpr::Term(val) => { - format!("{}{}", "\t".repeat(depth), val) - } + PrecClimber::new(vec![ + Operator::new(add, Left) | Operator::new(sub, Left) | Operator::new(modulo, Left), + Operator::new(mul, Left) | Operator::new(div, Left), + Operator::new(dice, Left), + Operator::new(pow, Right), + ]) + }; } - } - pub fn compute(&self) -> f64 { - use self::CalcExpr::*; - use self::BinOp::*; - use self::UnaryOp::*; + let result = Calc::parse(calc, s.as_ref())?; - match self { - Binary(bop, e1, e2) => { - let r1 = e1.compute(); - let r2 = e2.compute(); - match bop { - Add => r1 + r2, - Sub => r1 - r2, - Mul => r1 * r2, - Div => r1 / r2, - Mod => r1 % r2, - Pow => r1.powf(r2), - Min => r1.min(r2), - Max => r1.max(r2), - DiceRoll => { - let dice_count = r1 as usize; - let dice_faces = r2 as usize; + fn eval_single_pair(pair: Pair<self::Rule>) -> f64 { + match pair.as_rule() { + oct | hex | binary => { + let base = match pair.as_rule() { + hex => 16, + oct => 8, + binary => 2, + _ => unreachable!(), + }; - let mut rng = thread_rng(); - (0..dice_count).map(|_| rng.gen_range(1, dice_faces + 1)).sum::<usize>() as f64 - } - } - }, - Unary(uop, e) => { - let r = e.compute(); + u64::from_str_radix(&pair.as_str()[2..], base).unwrap() as f64 + }, + float => pair.as_str().parse::<f64>().unwrap(), + expr | num => eval_expr(pair.into_inner()), + unary_expr => { + let mut p = pair.into_inner(); - match uop { - Neg => -r, - Log => r.ln(), - Sqrt => r.sqrt(), - Sgn => r.signum(), - Sin => r.sin(), - Cos => r.cos(), - Tan => r.tan(), - Factorial => statrs::function::gamma::gamma(r), - Exp => r.exp(), - Abs => r.abs(), - Ceil => r.ceil(), - Floor => r.floor(), - Round => r.round(), - } - }, - Term(v) => *v, - } - } -} + let op = p.next().unwrap(); + let arg = eval_expr(p); -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum BinOp { - Add, - Sub, - Mul, - Div, - Mod, - Pow, - Min, - Max, - DiceRoll, -} + match op.as_rule() { + log => arg.ln(), + sqrt => arg.sqrt(), + sgn => arg.signum(), -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -enum UnaryOp { - Log, - Sqrt, - Sgn, - Exp, - Sin, - Cos, - Tan, - Factorial, - Neg, - Ceil, - Floor, - Abs, - Round, -} + sin => arg.sin(), + cos => arg.cos(), + tan => arg.tan(), + asin => arg.asin(), + acos => arg.acos(), + atan => arg.atan(), -fn parse_expr(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - ws!(input, up_to_add_sub_mod) -} + sinh => arg.sinh(), + cosh => arg.cosh(), + tanh => arg.tanh(), + asinh => arg.asinh(), + acosh => arg.acosh(), + atanh => arg.atanh(), -fn parse_add_sub_mod(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - ws!(input, do_parse!( - tpl: tuple!(up_to_div_mul, ws!(one_of!("+-%")), parse_expr) >> - ({ - let (expr1, op, expr2) = tpl; - let op = match op { - '+' => BinOp::Add, - '-' => BinOp::Sub, - '%' => BinOp::Mod, - _ => unreachable!(), - }; - Box::new(CalcExpr::Binary(op, expr1, expr2)) - }) - )) -} + exp => arg.exp(), + abs => arg.abs(), + ceil => arg.ceil(), + floor => arg.floor(), + round => arg.round(), + _ => unreachable!(), + } + }, + binary_expr => { + let mut p = pair.into_inner(); -fn parse_div_mul(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - ws!(input, do_parse!( - tpl: tuple!(up_to_binary_prefix, ws!(one_of!("/*")), parse_expr) >> - ({ - let (expr1, op, expr2) = tpl; - let op = match op { - '*' => BinOp::Mul, - '/' => BinOp::Div, - '^' => BinOp::Pow, - _ => unreachable!(), - }; - Box::new(CalcExpr::Binary(op, expr1, expr2)) - }) - )) -} + let op = p.next().unwrap(); -fn parse_binary_prefix(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - ws!(input, do_parse!( - op: ws!(alt_complete!( - tag!("min") => { |_| BinOp::Min } | - tag!("max") => { |_| BinOp::Max } - )) >> - expr1: up_to_unary_prefix >> - expr2: up_to_unary_prefix >> - (Box::new(CalcExpr::Binary(op, expr1, expr2))) - )) -} + let arg1 = eval_single_pair(p.next().unwrap()); + let arg2 = eval_single_pair(p.next().unwrap()); -fn parse_unary_prefix(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - ws!(input, do_parse!( - op: ws!(alt_complete!( - tag!("log") => { |_| UnaryOp::Log } - | tag!("sqrt") => { |_| UnaryOp::Sqrt } - | tag!("sin") => { |_| UnaryOp::Sin } - | tag!("cos") => { |_| UnaryOp::Cos } - | tag!("tan") => { |_| UnaryOp::Tan } - | tag!("sgn") => { |_| UnaryOp::Sgn } - | tag!("exp") => { |_| UnaryOp::Exp } - | tag!("ceil") => { |_| UnaryOp::Ceil } - | tag!("floor") => { |_| UnaryOp::Floor } - | tag!("abs") => { |_| UnaryOp::Abs } - | tag!("round") => { |_| UnaryOp::Round } - )) >> - expr: up_to_dice >> - (Box::new(CalcExpr::Unary(op, expr))) - )) -} + assert!(p.next().is_none()); -fn parse_dice(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - ws!(input, do_parse!( - tpl: separated_pair!(up_to_pow, ws!(char!('d')), up_to_pow) >> - ({ - let (expr1, expr2) = tpl; - Box::new(CalcExpr::Binary(BinOp::DiceRoll, expr1, expr2)) - }) - )) -} + match op.as_rule() { + min => arg1.min(arg2), + max => arg1.max(arg2), + atan2 => arg1.atan2(arg2), + _ => unreachable!(), + } + }, + suffix_expr => { + let mut p = pair.into_inner(); -fn parse_pow(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - ws!(input, do_parse!( - tpl: separated_pair!(up_to_neg, ws!(char!('^')), up_to_neg) >> - ({ - let (expr1, expr2) = tpl; - Box::new(CalcExpr::Binary(BinOp::Pow, expr1, expr2)) - }) - )) -} + let arg = eval_expr(p.next().unwrap().into_inner()); + let op = p.next().unwrap(); -fn parse_neg(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - ws!(input, do_parse!( - expr: ws!(preceded!(char!('-'), up_to_suffix)) >> - (Box::new(CalcExpr::Unary(UnaryOp::Neg, expr))) - )) -} + assert!(p.next().is_none()); -fn parse_suffix(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - ws!(input, do_parse!( - expr: terminated!(parse_term_or_paren, ws!(tag!("!"))) >> - (Box::new(CalcExpr::Unary(UnaryOp::Factorial, expr))) - )) -} + match op.as_rule() { + factorial => statrs::function::gamma::gamma(arg + 1.), + _ => unreachable!(), + } + }, + _ => unreachable!(), + } + } -fn parse_term_or_paren(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - ws!(input, alt_complete!( - delimited!(char!('('), parse_expr, char!(')')) | - do_parse!( - dat: double >> - (Box::new(CalcExpr::Term(dat))) - ) - )) -} + fn eval_expr(p: Pairs<self::Rule>) -> f64 { + CLIMBER.climb( + p, + eval_single_pair, + |lhs: f64, op, rhs: f64| match op.as_rule() { + add => lhs + rhs, + sub => lhs - rhs, + mul => lhs * rhs, + div => lhs / rhs, + pow => lhs.powf(rhs), + dice => { + let dice_count = lhs as usize; + let dice_faces = rhs as usize; -macro_rules! up_to { - ($up_to_name:ident, $fn_name:ident, $prev:ident) => ( - fn $up_to_name(input: CompleteStr) -> nom::IResult<CompleteStr, Box<CalcExpr>> { - alt_complete!(input, $fn_name | $prev) + let mut rng = thread_rng(); + (0..dice_count).map(|_| rng.gen_range(1, dice_faces + 1)).sum::<usize>() as f64 + }, + _ => unreachable!(), + } + ) } - ) -} -up_to! { up_to_add_sub_mod, parse_add_sub_mod, up_to_div_mul } -up_to! { up_to_div_mul, parse_div_mul, up_to_binary_prefix } -up_to! { up_to_binary_prefix, parse_binary_prefix, up_to_unary_prefix } -up_to! { up_to_unary_prefix, parse_unary_prefix, up_to_dice } -up_to! { up_to_dice, parse_dice, up_to_pow } -up_to! { up_to_pow, parse_pow, up_to_neg } -up_to! { up_to_neg, parse_neg, up_to_suffix } -up_to! { up_to_suffix, parse_suffix, parse_term_or_paren } + Ok(eval_expr(result)) + } +} #[cfg(test)] mod test { use super::*; #[test] - fn test_parse_usize() { - let (s, expr) = parse_expr("123".into()).unwrap(); - assert_eq!(s.0, ""); - assert_eq!(expr, box CalcExpr::Term(123.)); + fn test_calc_basics() { + assert_eq!(3., Calc::eval("1 + 2").unwrap()); + assert_eq!(3.0f64.ln(), Calc::eval("log 3").unwrap()); + assert!(6. - Calc::eval("3!").unwrap() < 0.0001); + assert_eq!(3., Calc::eval("max 3 2").unwrap()); } #[test] - fn test_parens() { - let (s, expr) = parse_expr("(123)".into()).unwrap(); - assert_eq!(s.0, ""); - assert_eq!(expr, box CalcExpr::Term(123.)); + fn test_binary_unary() { + assert_eq!(3.0f64.ln(), Calc::eval("max log 3 log 2").unwrap()); } #[test] - fn test_infix() { - let (s, expr) = parse_expr("1 + 2".into()).unwrap(); - assert_eq!(s.0, ""); - assert_eq!(expr, box CalcExpr::Binary(BinOp::Add, box CalcExpr::Term(1.), box CalcExpr::Term(2.))) + fn test_prefix_suffix() { + assert!(6. - Calc::eval("abs 3!").unwrap() < 0.0001); } - } pub fn roll(_ctx: &mut Context, msg: &Message, args: Args) -> Result<()> { - match CalcExpr::parse(args.rest()) { - Ok(expr) => send(msg.channel_id, &format!("{}", expr.compute()), msg.tts), - Err(e) => { - let parse_err = e.downcast::<CalcParseError>().unwrap(); - if let CalcParseError::NotReadToEnd { remaining, .. } = parse_err { - error!("parsing '{}': failed to consume '{}'", args.rest(), remaining); - send(msg.channel_id, "I COULDN'T READ THAT YOU FUCK", msg.tts) - } else { - Err(parse_err.into()) - } + match Calc::eval(args.rest()) { + Ok(result) => { + debug!("got calc result '{}'", result); + send(msg.channel_id, &format!("{}", result), msg.tts) }, - } -} - -pub fn debug_expr(_ctx: &mut Context, msg: &Message, args: Args) -> Result<()> { - match CalcExpr::parse(args.rest()) { - Ok(expr) => send(msg.channel_id, &expr.pretty(), false), Err(e) => { - let parse_err = e.downcast::<CalcParseError>().unwrap(); - if let CalcParseError::NotReadToEnd { remaining, parsed } = parse_err { - send(msg.channel_id, &format!("parsed this expr: {}\nwith remaining text: '{}'", parsed.pretty(), remaining), false) - } else { - Err(parse_err.into()) - } - } + error!("error encountered reading calc '{}': {}", args.rest(), e); + send(msg.channel_id, "I COULDN'T READ THAT YOU FUCK", msg.tts) + }, } } diff --git a/src/main.rs b/src/main.rs index 177f726..1d1c6dd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -18,7 +18,8 @@ extern crate fnv; #[cfg_attr(test, macro_use)] extern crate itertools; #[macro_use] extern crate lazy_static; #[macro_use] extern crate log; -#[macro_use] extern crate nom; +extern crate pest; +#[macro_use] extern crate pest_derive; extern crate rand; extern crate regex; extern crate reqwest; |
