libdice 0.1.0

A package for parsing simple dice-rolling expressions
Documentation
use lazy_static::lazy_static;
use pest::{
    error::{ErrorVariant, InputLocation},
    iterators::Pairs,
    pratt_parser::PrattParser,
    Parser,
};
use pest_derive::Parser;

#[derive(Parser)]
#[grammar = "grammar.pest"]
pub struct DiceParser;

#[derive(Debug, Clone)]
pub enum Expr {
    Integer(i64),
    BinOp {
        lhs: Box<Expr>,
        op: BinOp,
        rhs: Box<Expr>,
    },
    UnOp {
        op: UnOp,
        inner: Box<Expr>,
    },
}

#[derive(Debug, Clone)]
pub enum BinOp {
    Add,
    Subtract,
    Multiply,
    Divide,
    Dice,
}

#[derive(Debug, Clone)]
pub enum UnOp {
    Minus,
    Dice,
}

lazy_static! {
    static ref PRATT_PARSER: PrattParser<Rule> = {
        use pest::pratt_parser::{Assoc::*, Op};
        use Rule::*;

        PrattParser::new()
            .op(Op::infix(add, Left) | Op::infix(subtract, Left))
            .op(Op::infix(multiply, Left) | Op::infix(divide, Left))
            .op(Op::prefix(minus))
            .op(Op::prefix(undice))
            .op(Op::infix(bindice, Left))
    };
}

#[derive(Debug, Clone)]
pub enum ParseError {
    MissingStartExpr(String, InputLocation),
    MissingEndExpr(String, InputLocation),
    InvalidInteger(String),
}

fn parse_pairs(pairs: Pairs<Rule>) -> Result<Expr, ParseError> {
    PRATT_PARSER
        .map_primary(|primary| match primary.as_rule() {
            Rule::integer => {
                Ok(Expr::Integer(primary.as_str().parse::<i64>().map_err(
                    |_| ParseError::InvalidInteger(primary.as_str().into()),
                )?))
            }
            _ => unreachable!(),
        })
        .map_infix(|lhs, op, rhs| {
            let op = match op.as_rule() {
                Rule::add => BinOp::Add,
                Rule::subtract => BinOp::Subtract,
                Rule::multiply => BinOp::Multiply,
                Rule::divide => BinOp::Divide,
                Rule::bindice => BinOp::Dice,
                _ => unreachable!(),
            };
            Ok(Expr::BinOp {
                lhs: Box::new(lhs?),
                op,
                rhs: Box::new(rhs?),
            })
        })
        .map_prefix(|op, inner| {
            let op = match op.as_rule() {
                Rule::minus => UnOp::Minus,
                Rule::undice => UnOp::Dice,
                _ => unreachable!(),
            };
            Ok(Expr::UnOp {
                op,
                inner: Box::new(inner?),
            })
        })
        .parse(pairs)
}

fn handle_error(error: pest::error::Error<Rule>) -> ParseError {
    match error.variant {
        ErrorVariant::CustomError { .. } => unreachable!(),
        ErrorVariant::ParsingError { ref positives, .. } => {
            if positives.contains(&Rule::expr) {
                ParseError::MissingStartExpr(error.line().into(), error.location)
            } else {
                ParseError::MissingEndExpr(error.line().into(), error.location)
            }
        }
    }
}

pub fn load(text: &str) -> Result<Expr, ParseError> {
    let mut pairs = DiceParser::parse(Rule::equation, text).map_err(handle_error)?;
    let first_item = pairs.next().unwrap().into_inner();
    parse_pairs(first_item)
}