tuck5 0.2.0

A pragmatic lexer/parser generator
Documentation
use crate::meta::{eval_prog_from_text, graph_with_tags};

use super::*;
use test_case::test_case;

pub fn calc_tokens<'a>(text: &str) -> Vec<Token<Vec<String>>> {
    meta::eval_prog_from_text(
        "
         ## recognize words
            (e.g. sqrt, abs)
         ##
        % {
        a..z | A..Z | '_'. letter;
        letter+. word;

         ## recognize digits and positive integers
            (integers cannot have leading zeroes - maybe change this? It's
            mainly to show off that it's possible...)
         ##
        1..9. nonzero, digit;
        0. digit;
        nonzero & digit*. int, positive, number, expr;
        '0'. int, positive, number, expr;

          # recognize decimals and negative numbers
        int & '.' & int+. decimal, positive, number, expr;
        '-' & positive: negative, number, expr;

          # remove whitespace
        ws~;
        }

         ## The members of \"PEMDAS\" you know and love, along with function
            calls.
         ##
        {
            '(' & expr & ')': parens, expr;
            word & parens: call, expr;
            expr & '*' | '/' & expr: oper, expr;
            expr & '+' | '-' & expr: oper, expr;
        }
    ",
        text,
    )
}

pub fn eval(token: &Token<'_, Vec<String>>) -> Option<f64> {
    if !token.data.contains(&"expr".to_string()) {
        None
    } else if token.data.contains(&"parens".to_string()) {
        if let TokenType::Branch(children) = &token.t_type {
            eval(children.get(1)?)
        } else {
            None
        }
    } else if token.data.contains(&"number".to_string()) {
        Some(
            token
                .content()
                .parse()
                .expect("f64 was recognized as correct but didn't parse in Rust"),
        )
    } else if token.data.contains(&"oper".to_string()) {
        if let TokenType::Branch(children) = &token.t_type {
            match children.get(1)?.content() {
                "+" => Some(eval(children.get(0)?)? + eval(children.get(2)?)?),
                "-" => Some(eval(children.get(0)?)? - eval(children.get(2)?)?),
                "*" => Some(eval(children.get(0)?)? * eval(children.get(2)?)?),
                "/" => Some(eval(children.get(0)?)? / eval(children.get(2)?)?),
                _ => None,
            }
        } else {
            None
        }
    } else if token.data.contains(&"call".to_string()) {
        if let TokenType::Branch(children) = &token.t_type {
            match children.first()?.content() {
                "sqrt" => Some(eval(children.get(1)?)?.sqrt()),
                "abs" => Some(eval(children.get(1)?)?.abs()),
                "ln" => Some(eval(children.get(1)?)?.ln()),
                _ => None,
            }
        } else {
            None
        }
    } else {
        None
    }
}

pub fn eval_first(tokens: &Vec<Token<'_, Vec<String>>>) -> Option<f64> {
    graph_with_tags(tokens);
    if tokens.len() < 2 {
        eval(tokens.first()?)
    } else {
        None
    }
}

pub fn eval_text(text: &str) -> Option<f64> {
    eval_first(&calc_tokens(text))
}

#[test_case("1", Some(1.0); "one-digit number")]
#[test_case("a", None; "basic letter fail")]
#[test_case("", None; "no text")]
#[test_case(" 101", Some(101.0); "leading space")]
#[test_case("0101", None; "leading zero")]
#[test_case("123", Some(123.0); "multiple-digit number")]
#[test_case("123.0", Some(123.0); "multiple-digit number with decimal")]
#[test_case("0", Some(0.0); "zero")]
#[test_case("-1", Some(-1.0); "negative integer")]
#[test_case("-123.0", Some(-123.0); "negative decimal")]
#[test_case("1 + 1", Some(2.0); "basic integer addition")]
#[test_case("1 - 1", Some(0.0); "basic integer subtracting")]
#[test_case("2 * 3", Some(6.0); "basic integer multiplication")]
#[test_case("6 / 3", Some(2.0); "basic integer division")]
#[test_case("1 + 2 * 3 + 1", Some(8.0); "order of operations")]
#[test_case("(1)", Some(1.0); "basic parenthetical")]
#[test_case("1 + 1 * ((((50))))", Some(51.0); "order of operations with nested parens")]
#[test_case("((((50)))) * 1 + 1", Some(51.0); "order of operations with nested parens, backwards")]
#[test_case("sqrt(1)", Some(1.0); "basic application")]
#[test_case("sqrt(abs(ln(1)))", Some(0.0); "nested applications")]
#[test_case("sqrt(abs(ln(1)", None; "no trailing end-parens")]
pub fn eval_test(text: &str, expected: Option<f64>) {
    assert_eq!(eval_text(text), expected)
}