1use crate::diag::{OpyError, OpyResult, Position, Span};
9
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
12pub enum TokenKind {
13 Ident,
15 Number,
17 String,
19 Directive,
21 RulePrefixMarker,
24 At,
26 Newline,
27 Indent(u32),
29 Eof,
31 LParen,
33 RParen,
34 LBracket,
35 RBracket,
36 LBrace,
37 RBrace,
38 Comma,
39 Colon,
40 Semicolon,
41 Dot,
42 Assign,
43 Plus,
44 Minus,
45 Star,
46 Slash,
47 Percent,
48 DoubleStar,
49 PlusAssign,
50 MinusAssign,
51 Increment,
52 Decrement,
53 StarAssign,
54 SlashAssign,
55 PercentAssign,
56 DoubleStarAssign,
57 Eq,
58 Ne,
59 Lt,
60 Le,
61 Gt,
62 Ge,
63 LexBang,
65}
66
67#[derive(Debug, Clone, PartialEq)]
69pub struct Token {
70 pub kind: TokenKind,
71 pub text: String,
74 pub raw: Option<String>,
79 pub span: Span,
80}
81
82impl Token {
83 fn new(kind: TokenKind, text: impl Into<String>, span: Span) -> Token {
84 Token {
85 kind,
86 text: text.into(),
87 raw: None,
88 span,
89 }
90 }
91}
92
93pub struct LexInput<'a> {
95 pub file_id: u32,
96 pub text: &'a str,
97}
98
99pub fn lex(input: LexInput<'_>) -> OpyResult<Vec<Token>> {
101 Lexer::new(input.file_id, input.text).run()
102}
103
104struct Lexer {
105 file_id: u32,
106 chars: Vec<char>,
107 pos: usize,
108 line: u32,
109 col: u32,
110 tokens: Vec<Token>,
111}
112
113impl Lexer {
114 fn new(file_id: u32, text: &str) -> Lexer {
115 Lexer {
116 file_id,
117 chars: text.chars().collect(),
118 pos: 0,
119 line: 1,
120 col: 1,
121 tokens: Vec::new(),
122 }
123 }
124
125 fn run(mut self) -> OpyResult<Vec<Token>> {
126 while self.pos < self.chars.len() {
127 let ch = self.chars[self.pos];
128 match ch {
129 '\n' => {
130 self.tokens
131 .push(Token::new(TokenKind::Newline, "\n", self.here(1)));
132 self.advance();
133 self.line += 1;
134 self.col = 1;
135 }
136 ' ' | '\t' | '\r' => {
137 self.advance();
138 }
139 '\\' => {
140 if !self.skip_line_continuation() {
141 return Err(OpyError::at(
142 "lex-error",
143 "unexpected character '\\'",
144 self.here(1),
145 ));
146 }
147 }
148 '#' => self.lex_hash()?,
149 '/' if self.peek(1) == Some('*') => self.skip_block_comment()?,
150 '"' | '\'' => self.lex_string(ch)?,
151 c if c.is_ascii_digit() => self.lex_number()?,
152 c if is_ident_start(c) => self.lex_ident(),
153 '(' => self.single(TokenKind::LParen),
154 ')' => self.single(TokenKind::RParen),
155 '[' => self.single(TokenKind::LBracket),
156 ']' => self.single(TokenKind::RBracket),
157 '{' => self.single(TokenKind::LBrace),
158 '}' => self.single(TokenKind::RBrace),
159 ',' => self.single(TokenKind::Comma),
160 ':' => self.single(TokenKind::Colon),
161 ';' => self.single(TokenKind::Semicolon),
162 '.' => self.single(TokenKind::Dot),
163 '@' => self.single(TokenKind::At),
164 '=' => self.two(TokenKind::Assign, TokenKind::Eq, '='),
165 '+' => {
166 if self.peek(1) == Some('+') {
167 self.lex_duplicate(TokenKind::Increment, "++");
168 } else {
169 self.lex_two(TokenKind::Plus, TokenKind::PlusAssign, '=');
170 }
171 }
172 '-' => {
173 if self.peek(1) == Some('-') {
174 self.lex_duplicate(TokenKind::Decrement, "--");
175 } else {
176 self.lex_two(TokenKind::Minus, TokenKind::MinusAssign, '=');
177 }
178 }
179 '*' => {
180 if self.peek(1) == Some('*') {
181 if self.peek(2) == Some('=') {
182 let start = self.here(3);
183 self.advance();
184 self.advance();
185 self.advance();
186 let end = self.here(0);
187 self.tokens.push(Token::new(
188 TokenKind::DoubleStarAssign,
189 "**=",
190 Span::new(self.file_id, start.start, end.start),
191 ));
192 } else {
193 self.advance();
194 self.single(TokenKind::DoubleStar)
195 }
196 } else {
197 self.lex_two(TokenKind::Star, TokenKind::StarAssign, '=')
198 }
199 }
200 '/' => self.lex_two(TokenKind::Slash, TokenKind::SlashAssign, '='),
201 '%' => self.lex_two(TokenKind::Percent, TokenKind::PercentAssign, '='),
202 '<' => self.two(TokenKind::Lt, TokenKind::Le, '='),
203 '>' => self.two(TokenKind::Gt, TokenKind::Ge, '='),
204 '!' => self.two(TokenKind::LexBang, TokenKind::Ne, '='),
205 other => {
206 return Err(OpyError::at(
207 "lex-error",
208 format!("unexpected character '{other}'"),
209 self.here(1),
210 ));
211 }
212 }
213 }
214 let here = self.here(0);
215 self.tokens.push(Token::new(TokenKind::Eof, "", here));
216 Ok(self.tokens)
217 }
218
219 fn lex_hash(&mut self) -> OpyResult<()> {
221 if self.peek(1) == Some('!') {
222 let start = self.here(2);
223 self.advance();
224 self.advance();
225 let mut text = String::new();
226 while self.pos < self.chars.len() {
227 if self.chars[self.pos] == '\\' && self.skip_line_continuation() {
228 continue;
229 }
230 if self.chars[self.pos] == '\n' {
231 break;
232 }
233 text.push(self.chars[self.pos]);
234 self.advance();
235 }
236 let end = self.here(0);
237 self.tokens.push(Token::new(
238 TokenKind::Directive,
239 text,
240 Span::new(self.file_id, start.start, end.start),
241 ));
242 } else {
243 while self.pos < self.chars.len() && self.chars[self.pos] != '\n' {
244 self.advance();
245 }
246 }
247 Ok(())
248 }
249
250 fn skip_block_comment(&mut self) -> OpyResult<()> {
251 let start = self.here(2);
252 self.advance();
253 self.advance();
254 while self.pos < self.chars.len() {
255 if self.chars[self.pos] == '*' && self.peek(1) == Some('/') {
256 self.advance();
257 self.advance();
258 return Ok(());
259 }
260 if self.chars[self.pos] == '\n' {
261 self.advance();
262 self.line += 1;
263 self.col = 1;
264 } else {
265 self.advance();
266 }
267 }
268 Err(OpyError::at(
269 "lex-error",
270 "unterminated block comment",
271 start,
272 ))
273 }
274
275 fn lex_string(&mut self, quote: char) -> OpyResult<()> {
276 let start = self.here(1);
277 self.advance();
278 let mut value = String::new();
279 let mut raw = String::new();
280 while self.pos < self.chars.len() {
281 let ch = self.chars[self.pos];
282 if ch == quote {
283 self.advance();
284 let end = self.here(0);
285 let mut token = Token::new(
286 TokenKind::String,
287 value,
288 Span::new(self.file_id, start.start, end.start),
289 );
290 token.raw = Some(raw);
291 self.tokens.push(token);
292 return Ok(());
293 }
294 if ch == '\\' {
295 raw.push(ch);
296 self.advance();
297 if self.pos >= self.chars.len() {
298 break;
299 }
300 let escaped = self.chars[self.pos];
301 raw.push(escaped);
302 value.push(match escaped {
303 'n' => '\n',
304 't' => '\t',
305 'r' => '\r',
306 '\\' => '\\',
307 '"' => '"',
308 '\'' => '\'',
309 other => other,
310 });
311 self.advance();
312 continue;
313 }
314 if ch == '\n' {
315 return Err(OpyError::at(
316 "lex-error",
317 "unterminated string literal",
318 start,
319 ));
320 }
321 raw.push(ch);
322 value.push(ch);
323 self.advance();
324 }
325 Err(OpyError::at(
326 "lex-error",
327 "unterminated string literal",
328 start,
329 ))
330 }
331
332 fn skip_line_continuation(&mut self) -> bool {
333 let mut offset = 1;
334 while matches!(self.peek(offset), Some(' ' | '\r')) {
335 offset += 1;
336 }
337 if self.peek(offset) != Some('\n') {
338 return false;
339 }
340 for _ in 0..=offset {
341 self.advance();
342 }
343 self.line += 1;
344 self.col = 1;
345 true
346 }
347
348 fn lex_number(&mut self) -> OpyResult<()> {
349 let start = self.here(1);
350 let mut text = String::new();
351 if self.chars[self.pos] == '0' && matches!(self.peek(1), Some('x' | 'X')) {
352 text.push('0');
353 self.advance();
354 text.push(self.chars[self.pos]);
355 self.advance();
356 let digits_start = self.pos;
357 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_hexdigit() {
358 text.push(self.chars[self.pos]);
359 self.advance();
360 }
361 if self.pos == digits_start {
362 return Err(OpyError::at(
363 "lex-error",
364 "hexadecimal literal requires at least one hexadecimal digit",
365 Span::new(self.file_id, start.start, self.here(0).start),
366 ));
367 }
368 let end = self.here(0);
369 self.tokens.push(Token::new(
370 TokenKind::Number,
371 text,
372 Span::new(self.file_id, start.start, end.start),
373 ));
374 return Ok(());
375 }
376 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
377 text.push(self.chars[self.pos]);
378 self.advance();
379 }
380 if self.pos < self.chars.len()
381 && self.chars[self.pos] == '.'
382 && self.peek(1).is_some_and(|c| c.is_ascii_digit())
383 {
384 text.push('.');
385 self.advance();
386 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
387 text.push(self.chars[self.pos]);
388 self.advance();
389 }
390 }
391 if self.pos < self.chars.len()
394 && (self.chars[self.pos] == 'e' || self.chars[self.pos] == 'E')
395 {
396 let mut lookahead = self.pos + 1;
397 if lookahead < self.chars.len()
398 && (self.chars[lookahead] == '+' || self.chars[lookahead] == '-')
399 {
400 lookahead += 1;
401 }
402 if lookahead < self.chars.len() && self.chars[lookahead].is_ascii_digit() {
403 text.push('e');
404 self.advance();
405 if self.pos < self.chars.len()
406 && (self.chars[self.pos] == '+' || self.chars[self.pos] == '-')
407 {
408 text.push(self.chars[self.pos]);
409 self.advance();
410 }
411 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
412 text.push(self.chars[self.pos]);
413 self.advance();
414 }
415 }
416 }
417 let end = self.here(0);
418 self.tokens.push(Token::new(
419 TokenKind::Number,
420 text,
421 Span::new(self.file_id, start.start, end.start),
422 ));
423 Ok(())
424 }
425
426 fn lex_ident(&mut self) {
427 let start = self.here(1);
428 let mut text = String::new();
429 while self.pos < self.chars.len() && is_ident_continue(self.chars[self.pos]) {
430 text.push(self.chars[self.pos]);
431 self.advance();
432 }
433 let end = self.here(0);
434 self.tokens.push(Token::new(
435 TokenKind::Ident,
436 text,
437 Span::new(self.file_id, start.start, end.start),
438 ));
439 }
440
441 fn single(&mut self, kind: TokenKind) {
442 let start = self.here(1);
443 let text = self.chars[self.pos].to_string();
444 self.advance();
445 let end = self.here(0);
446 self.tokens.push(Token::new(
447 kind,
448 text,
449 Span::new(self.file_id, start.start, end.start),
450 ));
451 }
452
453 fn lex_two(&mut self, plain: TokenKind, assign: TokenKind, second: char) {
455 let start = self.here(1);
456 if self.peek(1) == Some(second) {
457 self.advance();
458 let text = format!("{}{}", self.chars[self.pos - 1], second);
459 self.advance();
460 let end = self.here(0);
461 self.tokens.push(Token::new(
462 assign,
463 text,
464 Span::new(self.file_id, start.start, end.start),
465 ));
466 } else {
467 let text = self.chars[self.pos].to_string();
468 self.advance();
469 let end = self.here(0);
470 self.tokens.push(Token::new(
471 plain,
472 text,
473 Span::new(self.file_id, start.start, end.start),
474 ));
475 }
476 }
477
478 fn lex_duplicate(&mut self, kind: TokenKind, text: &str) {
479 let start = self.here(1);
480 self.advance();
481 self.advance();
482 let end = self.here(0);
483 self.tokens.push(Token::new(
484 kind,
485 text,
486 Span::new(self.file_id, start.start, end.start),
487 ));
488 }
489
490 fn two(&mut self, plain: TokenKind, combined: TokenKind, second: char) {
492 let start = self.here(1);
493 let text = self.chars[self.pos].to_string();
494 if self.peek(1) == Some(second) {
495 self.advance();
496 let combined_text = format!("{}{}", text, second);
497 self.advance();
498 let end = self.here(0);
499 self.tokens.push(Token::new(
500 combined,
501 combined_text,
502 Span::new(self.file_id, start.start, end.start),
503 ));
504 } else {
505 self.advance();
506 let end = self.here(0);
507 self.tokens.push(Token::new(
508 plain,
509 text,
510 Span::new(self.file_id, start.start, end.start),
511 ));
512 }
513 }
514
515 fn here(&self, width: usize) -> Span {
516 Span::new(
517 self.file_id,
518 Position::new(self.line, self.col),
519 Position::new(self.line, self.col + width as u32),
520 )
521 }
522
523 fn peek(&self, offset: usize) -> Option<char> {
524 self.chars.get(self.pos + offset).copied()
525 }
526
527 fn advance(&mut self) {
528 self.pos += 1;
529 self.col += 1;
530 }
531}
532
533fn is_ident_start(c: char) -> bool {
534 c.is_ascii_alphabetic() || c == '_'
535}
536
537fn is_ident_continue(c: char) -> bool {
538 c.is_ascii_alphanumeric() || c == '_'
539}
540
541#[cfg(test)]
542mod tests {
543 use super::*;
544
545 fn lex_ok(text: &str) -> Vec<Token> {
546 lex(LexInput { file_id: 0, text }).unwrap()
547 }
548
549 #[test]
550 fn lexes_basic_rule() {
551 let tokens = lex_ok("rule \"setup\":\n @Event global\n disableInspector()\n");
552 let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind).collect();
553 assert!(kinds.contains(&TokenKind::Ident));
554 assert!(kinds.contains(&TokenKind::String));
555 assert!(kinds.contains(&TokenKind::Colon));
556 assert!(kinds.contains(&TokenKind::At));
557 assert!(kinds.contains(&TokenKind::LParen));
558 assert!(kinds.contains(&TokenKind::Eof));
559 }
560
561 #[test]
562 fn numbers_preserve_text() {
563 let tokens = lex_ok("1 2.5 0.016 100");
564 let numbers: Vec<&str> = tokens
565 .iter()
566 .filter(|t| t.kind == TokenKind::Number)
567 .map(|t| t.text.as_str())
568 .collect();
569 assert_eq!(numbers, vec!["1", "2.5", "0.016", "100"]);
570 }
571
572 #[test]
573 fn directives_and_comments() {
574 let tokens = lex_ok("#!define X 1\n# comment\nrule \"r\":\n");
575 let directive = tokens
576 .iter()
577 .find(|t| t.kind == TokenKind::Directive)
578 .unwrap();
579 assert_eq!(directive.text, "define X 1");
580 assert!(!tokens.iter().any(|t| t.text == "comment"));
581 }
582
583 #[test]
584 fn directive_line_continuation_is_part_of_one_directive() {
585 let tokens = lex_ok("#!define X first + \\\n second\n");
586 let directive = tokens
587 .iter()
588 .find(|token| token.kind == TokenKind::Directive)
589 .unwrap();
590 assert_eq!(directive.text, "define X first + second");
591 assert_eq!(directive.span.start, Position::new(1, 1));
592 assert_eq!(directive.span.end, Position::new(2, 9));
593 }
594
595 #[test]
596 fn operators() {
597 let tokens = lex_ok("a += b == c <= d != e / f");
598 let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind).collect();
599 for expected in [
600 TokenKind::PlusAssign,
601 TokenKind::Eq,
602 TokenKind::Le,
603 TokenKind::Ne,
604 TokenKind::Slash,
605 ] {
606 assert!(
607 kinds.contains(&expected),
608 "missing {expected:?} in {kinds:?}"
609 );
610 }
611 }
612
613 #[test]
614 fn power_operators_disambiguate() {
615 let tokens = lex_ok("a **= b ** c *= d");
618 let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind).collect();
619 assert_eq!(
620 kinds,
621 vec![
622 TokenKind::Ident,
623 TokenKind::DoubleStarAssign,
624 TokenKind::Ident,
625 TokenKind::DoubleStar,
626 TokenKind::Ident,
627 TokenKind::StarAssign,
628 TokenKind::Ident,
629 TokenKind::Eof,
630 ]
631 );
632 let assign = tokens
633 .iter()
634 .find(|t| t.kind == TokenKind::DoubleStarAssign)
635 .unwrap();
636 assert_eq!(assign.text, "**=");
637 }
638
639 #[test]
640 fn postfix_operators_are_single_tokens() {
641 let tokens = lex_ok("counter++ points--");
642 assert_eq!(
643 tokens.iter().map(|token| token.kind).collect::<Vec<_>>(),
644 vec![
645 TokenKind::Ident,
646 TokenKind::Increment,
647 TokenKind::Ident,
648 TokenKind::Decrement,
649 TokenKind::Eof,
650 ]
651 );
652 assert_eq!(tokens[1].span.start.col, 8);
653 assert_eq!(tokens[1].span.end.col, 10);
654 }
655
656 #[test]
657 fn unterminated_string_is_structured() {
658 let error = lex(LexInput {
659 file_id: 0,
660 text: "rule \"x\n",
661 })
662 .unwrap_err();
663 assert_eq!(error.code, "lex-error");
664 assert!(error.span.is_some());
665 }
666
667 #[test]
668 fn backslash_line_continuation_is_not_a_token() {
669 let tokens = lex_ok("one \\\ntwo");
670 assert_eq!(
671 tokens.iter().map(|token| token.kind).collect::<Vec<_>>(),
672 vec![TokenKind::Ident, TokenKind::Ident, TokenKind::Eof]
673 );
674 assert_eq!(tokens[1].span.start.line, 2);
675 assert_eq!(tokens[1].span.start.col, 1);
676 }
677
678 #[test]
679 fn crlf_line_continuation_tracks_the_next_line() {
680 let tokens = lex_ok("one \\\r\ntwo");
681 assert_eq!(tokens[1].span.start, Position::new(2, 1));
682 }
683
684 #[test]
685 fn whitespace_before_line_ending_is_part_of_the_continuation() {
686 let tokens = lex_ok("one \\ \ntwo");
687 assert_eq!(tokens[1].span.start, Position::new(2, 1));
688 }
689
690 #[test]
691 fn non_newline_backslash_remains_a_lex_error() {
692 for text in ["one \\ two", "one \\", "one \\ \t\ntwo"] {
693 let error = lex(LexInput { file_id: 0, text }).unwrap_err();
694 assert_eq!(error.code, "lex-error");
695 assert_eq!(error.message, "unexpected character '\\'");
696 assert_eq!(error.span.unwrap().start, Position::new(1, 5));
697 }
698 }
699}