1use fastn_resolved::evalexpr::{
2    error::{EvalexprError, EvalexprResult},
3    value::{FloatType, IntType},
4};
5
6mod display;
7
8#[derive(Clone, PartialEq, Debug)]
9pub enum Token {
10    Plus,
12    Minus,
13    Star,
14    Slash,
15    Percent,
16    Hat,
17
18    Eq,
20    Neq,
21    Gt,
22    Lt,
23    Geq,
24    Leq,
25    And,
26    Or,
27    Not,
28
29    LBrace,
31    RBrace,
32
33    Assign,
35    PlusAssign,
36    MinusAssign,
37    StarAssign,
38    SlashAssign,
39    PercentAssign,
40    HatAssign,
41    AndAssign,
42    OrAssign,
43
44    Comma,
46    Semicolon,
47
48    Identifier(String),
50    Float(FloatType),
51    Int(IntType),
52    Boolean(bool),
53    String(String),
54}
55
56#[derive(Clone, Debug, PartialEq)]
58pub enum PartialToken {
59    Token(Token),
61    Literal(String),
63    Plus,
65    Minus,
67    Star,
69    Slash,
71    Percent,
73    Hat,
75    Whitespace,
77    Eq,
79    ExclamationMark,
81    Gt,
83    Lt,
85    Ampersand,
87    VerticalBar,
89}
90
91fn char_to_partial_token(c: char) -> PartialToken {
93    match c {
94        '+' => PartialToken::Plus,
95        '-' => PartialToken::Minus,
96        '*' => PartialToken::Star,
97        '/' => PartialToken::Slash,
98        '%' => PartialToken::Percent,
99        '^' => PartialToken::Hat,
100
101        '(' => PartialToken::Token(Token::LBrace),
102        ')' => PartialToken::Token(Token::RBrace),
103
104        ',' => PartialToken::Token(Token::Comma),
105        ';' => PartialToken::Token(Token::Semicolon),
106
107        '=' => PartialToken::Eq,
108        '!' => PartialToken::ExclamationMark,
109        '>' => PartialToken::Gt,
110        '<' => PartialToken::Lt,
111        '&' => PartialToken::Ampersand,
112        '|' => PartialToken::VerticalBar,
113
114        c => {
115            if c.is_whitespace() {
116                PartialToken::Whitespace
117            } else {
118                PartialToken::Literal(c.to_string())
119            }
120        }
121    }
122}
123
124impl Token {
125    pub(crate) const fn is_leftsided_value(&self) -> bool {
126        match self {
127            Token::Plus => false,
128            Token::Minus => false,
129            Token::Star => false,
130            Token::Slash => false,
131            Token::Percent => false,
132            Token::Hat => false,
133
134            Token::Eq => false,
135            Token::Neq => false,
136            Token::Gt => false,
137            Token::Lt => false,
138            Token::Geq => false,
139            Token::Leq => false,
140            Token::And => false,
141            Token::Or => false,
142            Token::Not => false,
143
144            Token::LBrace => true,
145            Token::RBrace => false,
146
147            Token::Comma => false,
148            Token::Semicolon => false,
149
150            Token::Assign => false,
151            Token::PlusAssign => false,
152            Token::MinusAssign => false,
153            Token::StarAssign => false,
154            Token::SlashAssign => false,
155            Token::PercentAssign => false,
156            Token::HatAssign => false,
157            Token::AndAssign => false,
158            Token::OrAssign => false,
159
160            Token::Identifier(_) => true,
161            Token::Float(_) => true,
162            Token::Int(_) => true,
163            Token::Boolean(_) => true,
164            Token::String(_) => true,
165        }
166    }
167
168    pub(crate) const fn is_rightsided_value(&self) -> bool {
169        match self {
170            Token::Plus => false,
171            Token::Minus => false,
172            Token::Star => false,
173            Token::Slash => false,
174            Token::Percent => false,
175            Token::Hat => false,
176
177            Token::Eq => false,
178            Token::Neq => false,
179            Token::Gt => false,
180            Token::Lt => false,
181            Token::Geq => false,
182            Token::Leq => false,
183            Token::And => false,
184            Token::Or => false,
185            Token::Not => false,
186
187            Token::LBrace => false,
188            Token::RBrace => true,
189
190            Token::Comma => false,
191            Token::Semicolon => false,
192
193            Token::Assign => false,
194            Token::PlusAssign => false,
195            Token::MinusAssign => false,
196            Token::StarAssign => false,
197            Token::SlashAssign => false,
198            Token::PercentAssign => false,
199            Token::HatAssign => false,
200            Token::AndAssign => false,
201            Token::OrAssign => false,
202
203            Token::Identifier(_) => true,
204            Token::Float(_) => true,
205            Token::Int(_) => true,
206            Token::Boolean(_) => true,
207            Token::String(_) => true,
208        }
209    }
210
211    pub(crate) fn is_assignment(&self) -> bool {
212        use Token::*;
213        matches!(
214            self,
215            Assign
216                | PlusAssign
217                | MinusAssign
218                | StarAssign
219                | SlashAssign
220                | PercentAssign
221                | HatAssign
222                | AndAssign
223                | OrAssign
224        )
225    }
226}
227
228fn parse_escape_sequence<Iter: Iterator<Item = char>>(iter: &mut Iter) -> EvalexprResult<char> {
230    match iter.next() {
231        Some('"') => Ok('"'),
232        Some('\\') => Ok('\\'),
233        Some(c) => Err(EvalexprError::IllegalEscapeSequence(format!("\\{}", c))),
234        None => Err(EvalexprError::IllegalEscapeSequence("\\".to_string())),
235    }
236}
237
238fn parse_string_literal<Iter: Iterator<Item = char>>(
245    mut iter: &mut Iter,
246) -> EvalexprResult<PartialToken> {
247    let mut result = String::new();
248
249    while let Some(c) = iter.next() {
250        match c {
251            '"' => break,
252            '\\' => result.push(parse_escape_sequence(&mut iter)?),
253            c => result.push(c),
254        }
255    }
256
257    Ok(PartialToken::Token(Token::String(result)))
258}
259
260fn str_to_partial_tokens(string: &str) -> EvalexprResult<Vec<PartialToken>> {
262    let mut result = Vec::new();
263    let mut iter = string.chars().peekable();
264
265    while let Some(c) = iter.next() {
266        if c == '"' {
267            result.push(parse_string_literal(&mut iter)?);
268        } else {
269            let mut partial_token = char_to_partial_token(c);
270            if let Some(PartialToken::Literal(..)) = result.last() {
271                if partial_token == PartialToken::Minus {
272                    partial_token = PartialToken::Literal('-'.to_string())
273                }
274            }
275
276            let if_let_successful =
277                if let (Some(PartialToken::Literal(last)), PartialToken::Literal(literal)) =
278                    (result.last_mut(), &partial_token)
279                {
280                    last.push_str(literal);
281                    true
282                } else {
283                    false
284                };
285
286            if !if_let_successful {
287                result.push(partial_token);
288            }
289        }
290    }
291    Ok(result)
292}
293
294fn partial_tokens_to_tokens(mut tokens: &[PartialToken]) -> EvalexprResult<Vec<Token>> {
296    let mut result = Vec::new();
297    while !tokens.is_empty() {
298        let first = tokens[0].clone();
299        let second = tokens.get(1).cloned();
300        let third = tokens.get(2).cloned();
301        let mut cutoff = 2;
302
303        result.extend(
304            match first {
305                PartialToken::Token(token) => {
306                    cutoff = 1;
307                    Some(token)
308                }
309                PartialToken::Plus => match second {
310                    Some(PartialToken::Eq) => Some(Token::PlusAssign),
311                    _ => {
312                        cutoff = 1;
313                        Some(Token::Plus)
314                    }
315                },
316                PartialToken::Minus => match second {
317                    Some(PartialToken::Eq) => Some(Token::MinusAssign),
318                    _ => {
319                        cutoff = 1;
320                        Some(Token::Minus)
321                    }
322                },
323                PartialToken::Star => match second {
324                    Some(PartialToken::Eq) => Some(Token::StarAssign),
325                    _ => {
326                        cutoff = 1;
327                        Some(Token::Star)
328                    }
329                },
330                PartialToken::Slash => match second {
331                    Some(PartialToken::Eq) => Some(Token::SlashAssign),
332                    _ => {
333                        cutoff = 1;
334                        Some(Token::Slash)
335                    }
336                },
337                PartialToken::Percent => match second {
338                    Some(PartialToken::Eq) => Some(Token::PercentAssign),
339                    _ => {
340                        cutoff = 1;
341                        Some(Token::Percent)
342                    }
343                },
344                PartialToken::Hat => match second {
345                    Some(PartialToken::Eq) => Some(Token::HatAssign),
346                    _ => {
347                        cutoff = 1;
348                        Some(Token::Hat)
349                    }
350                },
351                PartialToken::Literal(literal) => {
352                    cutoff = 1;
353                    if let Ok(number) = literal.parse::<IntType>() {
354                        Some(Token::Int(number))
355                    } else if let Ok(number) = literal.parse::<FloatType>() {
356                        Some(Token::Float(number))
357                    } else if let Ok(boolean) = literal.parse::<bool>() {
358                        Some(Token::Boolean(boolean))
359                    } else {
360                        match (second, third) {
365                            (Some(second), Some(third))
366                                if second == PartialToken::Minus
367                                    || second == PartialToken::Plus =>
368                            {
369                                if let Ok(number) =
370                                    format!("{}{}{}", literal, second, third).parse::<FloatType>()
371                                {
372                                    cutoff = 3;
373                                    Some(Token::Float(number))
374                                } else {
375                                    Some(Token::Identifier(literal.to_string()))
376                                }
377                            }
378                            _ => Some(Token::Identifier(literal.to_string())),
379                        }
380                    }
381                }
382                PartialToken::Whitespace => {
383                    cutoff = 1;
384                    None
385                }
386                PartialToken::Eq => match second {
387                    Some(PartialToken::Eq) => Some(Token::Eq),
388                    _ => {
389                        cutoff = 1;
390                        Some(Token::Assign)
391                    }
392                },
393                PartialToken::ExclamationMark => match second {
394                    Some(PartialToken::Eq) => Some(Token::Neq),
395                    _ => {
396                        cutoff = 1;
397                        Some(Token::Not)
398                    }
399                },
400                PartialToken::Gt => match second {
401                    Some(PartialToken::Eq) => Some(Token::Geq),
402                    _ => {
403                        cutoff = 1;
404                        Some(Token::Gt)
405                    }
406                },
407                PartialToken::Lt => match second {
408                    Some(PartialToken::Eq) => Some(Token::Leq),
409                    _ => {
410                        cutoff = 1;
411                        Some(Token::Lt)
412                    }
413                },
414                PartialToken::Ampersand => match second {
415                    Some(PartialToken::Ampersand) => match third {
416                        Some(PartialToken::Eq) => {
417                            cutoff = 3;
418                            Some(Token::AndAssign)
419                        }
420                        _ => Some(Token::And),
421                    },
422                    _ => return Err(EvalexprError::unmatched_partial_token(first, second)),
423                },
424                PartialToken::VerticalBar => match second {
425                    Some(PartialToken::VerticalBar) => match third {
426                        Some(PartialToken::Eq) => {
427                            cutoff = 3;
428                            Some(Token::OrAssign)
429                        }
430                        _ => Some(Token::Or),
431                    },
432                    _ => return Err(EvalexprError::unmatched_partial_token(first, second)),
433                },
434            }
435            .into_iter(),
436        );
437
438        tokens = &tokens[cutoff..];
439    }
440    Ok(result)
441}
442
443pub(crate) fn tokenize(string: &str) -> EvalexprResult<Vec<Token>> {
444    partial_tokens_to_tokens(&str_to_partial_tokens(string)?)
445}
446
447#[cfg(test)]
448mod tests {
449    use fastn_resolved::evalexpr::token::{char_to_partial_token, tokenize, Token};
450    use std::fmt::Write;
451
452    #[test]
453    fn test_partial_token_display() {
454        let chars = vec![
455            '+', '-', '*', '/', '%', '^', '(', ')', ',', ';', '=', '!', '>', '<', '&', '|', ' ',
456        ];
457
458        for char in chars {
459            assert_eq!(
460                format!("{}", char),
461                format!("{}", char_to_partial_token(char))
462            );
463        }
464    }
465
466    #[test]
467    fn test_token_display() {
468        let token_string =
469            "+ - * / % ^ == != > < >= <= && || ! ( ) = += -= *= /= %= ^= &&= ||= , ; ";
470        let tokens = tokenize(token_string).unwrap();
471        let mut result_string = String::new();
472
473        for token in tokens {
474            write!(result_string, "{} ", token).unwrap();
475        }
476
477        assert_eq!(token_string, result_string);
478    }
479
480    #[test]
481    fn assignment_lhs_is_identifier() {
482        let tokens = tokenize("a = 1").unwrap();
483        assert_eq!(
484            tokens.as_slice(),
485            [
486                Token::Identifier("a".to_string()),
487                Token::Assign,
488                Token::Int(1)
489            ]
490        );
491    }
492}