wowlab_parsers/parsers/simc/
lexer.rs1use 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 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}