Skip to main content

wowlab_parsers/parsers/simc/
lexer.rs

1use logos::{Lexer as LogosLexer, Logos};
2
3fn parse_comment<'a>(lex: &mut LogosLexer<'a, Token<'a>>) -> &'a str {
4    let remainder = lex.remainder();
5    let end = remainder.find(['\r', '\n']).unwrap_or(remainder.len());
6
7    lex.bump(end);
8
9    lex.slice()
10}
11
12#[derive(Clone, Debug, Logos, PartialEq)]
13#[logos(skip r"[ \t]+")]
14pub(super) enum Token<'a> {
15    #[regex(r"-?[0-9]+", |lex| lex.slice().parse::<i64>().ok())]
16    Number(i64),
17
18    #[regex(r"-?[0-9]+\.[0-9]+")]
19    Float(&'a str),
20
21    #[regex(r#""[^"]*""#, |lex| { let s = lex.slice(); &s[1..s.len()-1] })]
22    String(&'a str),
23
24    #[regex(r"'[^']*'", |lex| { let s = lex.slice(); &s[1..s.len()-1] })]
25    SingleString(&'a str),
26
27    #[regex(r"[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z0-9_]+)*")]
28    Ident(&'a str),
29
30    #[token("=")]
31    Eq,
32
33    #[token(",")]
34    Comma,
35
36    #[token("/")]
37    Slash,
38
39    #[token("+")]
40    Plus,
41
42    #[token("-")]
43    Minus,
44
45    #[token("*")]
46    Star,
47
48    #[token("%")]
49    Percent,
50
51    #[token(":")]
52    Colon,
53
54    #[token("!")]
55    Bang,
56
57    #[token("&")]
58    Amp,
59
60    #[token("|")]
61    Pipe,
62
63    #[token("<")]
64    Lt,
65
66    #[token(">")]
67    Gt,
68
69    #[token("?")]
70    Question,
71
72    #[token("(")]
73    LParen,
74
75    #[token(")")]
76    RParen,
77
78    #[regex(r"\r?\n")]
79    Newline,
80
81    #[token("#", parse_comment)]
82    Comment(&'a str),
83}
84
85type Lexer<'a> = LogosLexer<'a, Token<'a>>;
86
87pub(super) fn lex(input: &str) -> Lexer<'_> {
88    Token::lexer(input)
89}
90
91#[cfg(test)]
92mod tests {
93    use googletest::prelude::*;
94    use rstest::rstest;
95
96    use super::*;
97
98    // #t(fn: rust_error_type_unit) logos default error type is ()
99    fn toks(input: &str) -> Result<Vec<Token<'_>>, ()> {
100        lex(input).collect()
101    }
102
103    #[gtest]
104    #[rstest]
105    #[case::number_positive("123", vec![Token::Number(123)])]
106    #[case::number_negative("-18", vec![Token::Number(-18)])]
107    #[case::number_zero("0", vec![Token::Number(0)])]
108    #[case::float_basic("1.5", vec![Token::Float("1.5")])]
109    #[case::float_negative("-0.3", vec![Token::Float("-0.3")])]
110    #[case::string_basic("\"hello world\"", vec![Token::String("hello world")])]
111    #[case::string_empty("\"\"", vec![Token::String("")])]
112    #[case::single_string_basic("'foo'", vec![Token::SingleString("foo")])]
113    #[case::single_string_empty("''", vec![Token::SingleString("")])]
114    #[case::ident_basic("warrior", vec![Token::Ident("warrior")])]
115    #[case::ident_underscore_start("_hidden", vec![Token::Ident("_hidden")])]
116    #[case::ident_alnum("spell_target_if2", vec![Token::Ident("spell_target_if2")])]
117    #[case::ident_dotted("warlock.soul_shards", vec![Token::Ident("warlock.soul_shards")])]
118    #[case::ident_dotted_deep("buff.rush.up", vec![Token::Ident("buff.rush.up")])]
119    #[case::ident_dotted_numeric_segment("apex.1", vec![Token::Ident("apex.1")])]
120    #[case::op_eq("=", vec![Token::Eq])]
121    #[case::op_comma(",", vec![Token::Comma])]
122    #[case::op_slash("/", vec![Token::Slash])]
123    #[case::op_plus("+", vec![Token::Plus])]
124    #[case::op_minus("-", vec![Token::Minus])]
125    #[case::op_colon(":", vec![Token::Colon])]
126    #[case::newline_lf("\n", vec![Token::Newline])]
127    #[case::newline_crlf("\r\n", vec![Token::Newline])]
128    #[case::comment_to_eol("# This is a comment\n", vec![Token::Comment("# This is a comment"), Token::Newline])]
129    #[case::comment_eof_no_newline("# trailing", vec![Token::Comment("# trailing")])]
130    #[case::comment_hash_only("#", vec![Token::Comment("#")])]
131    #[case::assignment_string("warrior=\"TestName\"", vec![Token::Ident("warrior"), Token::Eq, Token::String("TestName")])]
132    #[case::number_slash_number("123/456", vec![Token::Number(123), Token::Slash, Token::Number(456)])]
133    #[case::spaces_skipped("a = b", vec![Token::Ident("a"), Token::Eq, Token::Ident("b")])]
134    #[case::tabs_skipped("a\t=\tb", vec![Token::Ident("a"), Token::Eq, Token::Ident("b")])]
135    #[case::adjacent_operators("=,/+:", vec![Token::Eq, Token::Comma, Token::Slash, Token::Plus, Token::Colon])]
136    #[case::simc_apl_line("actions+=/spell", vec![Token::Ident("actions"), Token::Plus, Token::Eq, Token::Slash, Token::Ident("spell")])]
137    #[case::simc_apl_sublist("actions.precombat+=/flask", vec![Token::Ident("actions.precombat"), Token::Plus, Token::Eq, Token::Slash, Token::Ident("flask")])]
138    #[case::simc_condition(
139        "if=!(apex.1&buff.up)|health.pct<0.3*2",
140        vec![
141            Token::Ident("if"), Token::Eq, Token::Bang, Token::LParen, Token::Ident("apex.1"),
142            Token::Amp, Token::Ident("buff.up"), Token::RParen, Token::Pipe,
143            Token::Ident("health.pct"), Token::Lt, Token::Float("0.3"), Token::Star, Token::Number(2),
144        ]
145    )]
146    #[case::simc_max_operator("a>?3", vec![Token::Ident("a"), Token::Gt, Token::Question, Token::Number(3)])]
147    #[case::simc_modulo("a%3", vec![Token::Ident("a"), Token::Percent, Token::Number(3)])]
148    #[case::ident_minus_number("remains-3", vec![Token::Ident("remains"), Token::Number(-3)])]
149    #[case::ident_minus_ident("a-b", vec![Token::Ident("a"), Token::Minus, Token::Ident("b")])]
150    #[case::hyphen_binds_to_number("5-3", vec![Token::Number(5), Token::Number(-3)])]
151    #[case::assignment_negative("x=-5", vec![Token::Ident("x"), Token::Eq, Token::Number(-5)])]
152    #[case::colon_list("1:2:3", vec![Token::Number(1), Token::Colon, Token::Number(2), Token::Colon, Token::Number(3)])]
153    #[case::comment_then_content("# c\nx=1", vec![Token::Comment("# c"), Token::Newline, Token::Ident("x"), Token::Eq, Token::Number(1)])]
154    #[case::empty_input("", vec![])]
155    #[case::whitespace_only("   ", vec![])]
156    fn lexes_token_stream(#[case] input: &str, #[case] expected: Vec<Token<'_>>) -> Result<()> {
157        verify_that!(toks(input), ok(eq(&expected)))?;
158
159        Ok(())
160    }
161
162    #[gtest]
163    #[rstest]
164    #[case::number_overflow("99999999999999999999")]
165    #[case::unterminated_double_string("\"abc")]
166    #[case::unterminated_single_string("'abc")]
167    #[case::unknown_char_at("@")]
168    #[case::lone_cr("\r")]
169    #[case::lone_dot(".")]
170    #[case::trailing_dot("foo.")]
171    fn lex_errors(#[case] input: &str) -> Result<()> {
172        verify_that!(toks(input), err(anything()))?;
173
174        Ok(())
175    }
176}