1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
//* This file is part of the uutils coreutils package.
//*
//* (c) Roman Gafiyatullin <r.gafiyatullin@me.com>
//*
//* For the full copyright and license information, please view the LICENSE
//* file that was distributed with this source code.

//!
//! The following tokens are present in the expr grammar:
//! * integer literal;
//! * string literal;
//! * infix binary operators;
//! * prefix operators.
//!
//! According to the man-page of expr we have expression split into tokens (each token -- separate CLI-argument).
//! Hence all we need is to map the strings into the Token structures, except for some ugly fiddling with +-escaping.
//!

// spell-checker:ignore (ToDO) paren

use num_bigint::BigInt;

#[derive(Debug, Clone)]
pub enum Token {
    Value {
        value: String,
    },

    ParOpen,
    ParClose,

    InfixOp {
        precedence: u8,
        left_assoc: bool,
        value: String,
    },

    PrefixOp {
        arity: usize,
        value: String,
    },
}
impl Token {
    fn new_infix_op(v: &str, left_assoc: bool, precedence: u8) -> Self {
        Self::InfixOp {
            left_assoc,
            precedence,
            value: v.into(),
        }
    }
    fn new_value(v: &str) -> Self {
        Self::Value { value: v.into() }
    }

    fn is_infix_plus(&self) -> bool {
        match self {
            Self::InfixOp { value, .. } => value == "+",
            _ => false,
        }
    }
    fn is_a_number(&self) -> bool {
        match self {
            Self::Value { value, .. } => value.parse::<BigInt>().is_ok(),
            _ => false,
        }
    }
    fn is_a_close_paren(&self) -> bool {
        matches!(*self, Self::ParClose)
    }
}

pub fn strings_to_tokens(strings: &[&str]) -> Result<Vec<(usize, Token)>, String> {
    let mut tokens_acc = Vec::with_capacity(strings.len());
    let mut tok_idx = 1;

    for s in strings {
        let token_if_not_escaped = match *s {
            "(" => Token::ParOpen,
            ")" => Token::ParClose,

            "^" => Token::new_infix_op(s, false, 7),

            ":" => Token::new_infix_op(s, true, 6),

            "*" | "/" | "%" => Token::new_infix_op(s, true, 5),

            "+" | "-" => Token::new_infix_op(s, true, 4),

            "=" | "!=" | "<" | ">" | "<=" | ">=" => Token::new_infix_op(s, true, 3),

            "&" => Token::new_infix_op(s, true, 2),

            "|" => Token::new_infix_op(s, true, 1),

            "match" | "index" => Token::PrefixOp {
                arity: 2,
                value: s.to_string(),
            },
            "substr" => Token::PrefixOp {
                arity: 3,
                value: s.to_string(),
            },
            "length" => Token::PrefixOp {
                arity: 1,
                value: s.to_string(),
            },

            _ => Token::new_value(s),
        };
        push_token_if_not_escaped(&mut tokens_acc, tok_idx, token_if_not_escaped, s);
        tok_idx += 1;
    }
    maybe_dump_tokens_acc(&tokens_acc);

    Ok(tokens_acc)
}

fn maybe_dump_tokens_acc(tokens_acc: &[(usize, Token)]) {
    use std::env;

    if let Ok(debug_var) = env::var("EXPR_DEBUG_TOKENS") {
        if debug_var == "1" {
            println!("EXPR_DEBUG_TOKENS");
            for token in tokens_acc {
                println!("\t{token:?}");
            }
        }
    }
}

fn push_token_if_not_escaped(acc: &mut Vec<(usize, Token)>, tok_idx: usize, token: Token, s: &str) {
    // Smells heuristics... :(
    let prev_is_plus = match acc.last() {
        None => false,
        Some(t) => t.1.is_infix_plus(),
    };
    let should_use_as_escaped = if prev_is_plus && acc.len() >= 2 {
        let pre_prev = &acc[acc.len() - 2];
        !(pre_prev.1.is_a_number() || pre_prev.1.is_a_close_paren())
    } else {
        prev_is_plus
    };

    if should_use_as_escaped {
        acc.pop();
        acc.push((tok_idx, Token::new_value(s)));
    } else {
        acc.push((tok_idx, token));
    }
}