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