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,
83 pub(crate) layout: Span,
88}
89
90impl Token {
91 fn new(kind: TokenKind, text: impl Into<String>, span: Span) -> Token {
92 Token {
93 kind,
94 text: text.into(),
95 raw: None,
96 span,
97 layout: span,
98 }
99 }
100}
101
102pub struct LexInput<'a> {
104 pub file_id: u32,
105 pub text: &'a str,
106}
107
108pub fn lex(input: LexInput<'_>) -> OpyResult<Vec<Token>> {
110 Lexer::new(input.file_id, input.text).run()
111}
112
113struct Lexer {
114 file_id: u32,
115 chars: Vec<char>,
116 pos: usize,
117 line: u32,
118 col: u32,
119 tokens: Vec<Token>,
120}
121
122impl Lexer {
123 fn new(file_id: u32, text: &str) -> Lexer {
124 Lexer {
125 file_id,
126 chars: text.chars().collect(),
127 pos: 0,
128 line: 1,
129 col: 1,
130 tokens: Vec::new(),
131 }
132 }
133
134 fn run(mut self) -> OpyResult<Vec<Token>> {
135 while self.pos < self.chars.len() {
136 let ch = self.chars[self.pos];
137 match ch {
138 '\n' => {
139 self.tokens
140 .push(Token::new(TokenKind::Newline, "\n", self.here(1)));
141 self.advance();
142 self.line += 1;
143 self.col = 1;
144 }
145 ' ' | '\r' => {
146 self.advance();
147 }
148 '\t' => {
149 self.pos += 1;
150 self.col += 4;
151 }
152 '\\' => {
153 if !self.skip_line_continuation() {
154 return Err(OpyError::at(
155 "lex-error",
156 "unexpected character '\\'",
157 self.here(1),
158 ));
159 }
160 }
161 '#' => self.lex_hash(),
162 '/' if self.peek(1) == Some('*') => self.skip_block_comment()?,
163 '"' | '\'' => self.lex_string(ch)?,
164 c if c.is_ascii_digit() => self.lex_number()?,
165 c if is_ident_start(c) => self.lex_ident(),
166 '(' => self.single(TokenKind::LParen),
167 ')' => self.single(TokenKind::RParen),
168 '[' => self.single(TokenKind::LBracket),
169 ']' => self.single(TokenKind::RBracket),
170 '{' => self.single(TokenKind::LBrace),
171 '}' => self.single(TokenKind::RBrace),
172 ',' => self.single(TokenKind::Comma),
173 ':' => self.single(TokenKind::Colon),
174 ';' => self.single(TokenKind::Semicolon),
175 '.' => self.single(TokenKind::Dot),
176 '@' => self.single(TokenKind::At),
177 '=' => self.lex_two(TokenKind::Assign, TokenKind::Eq, '='),
178 '+' => {
179 if self.peek(1) == Some('+') {
180 self.lex_duplicate(TokenKind::Increment, "++");
181 } else {
182 self.lex_two(TokenKind::Plus, TokenKind::PlusAssign, '=');
183 }
184 }
185 '-' => {
186 if self.peek(1) == Some('-') {
187 self.lex_duplicate(TokenKind::Decrement, "--");
188 } else {
189 self.lex_two(TokenKind::Minus, TokenKind::MinusAssign, '=');
190 }
191 }
192 '*' => {
193 if self.peek(1) == Some('*') {
194 if self.peek(2) == Some('=') {
195 let start = self.here(3);
196 self.advance();
197 self.advance();
198 self.advance();
199 self.push_token(TokenKind::DoubleStarAssign, "**=", start);
200 } else {
201 self.advance();
202 self.single(TokenKind::DoubleStar)
203 }
204 } else {
205 self.lex_two(TokenKind::Star, TokenKind::StarAssign, '=')
206 }
207 }
208 '/' => self.lex_two(TokenKind::Slash, TokenKind::SlashAssign, '='),
209 '%' => self.lex_two(TokenKind::Percent, TokenKind::PercentAssign, '='),
210 '<' => self.lex_two(TokenKind::Lt, TokenKind::Le, '='),
211 '>' => self.lex_two(TokenKind::Gt, TokenKind::Ge, '='),
212 '!' => self.lex_two(TokenKind::LexBang, TokenKind::Ne, '='),
213 other => {
214 return Err(OpyError::at(
215 "lex-error",
216 format!("unexpected character '{other}'"),
217 self.here(1),
218 ));
219 }
220 }
221 }
222 let here = self.here(0);
223 self.push_token(TokenKind::Eof, "", here);
224 Ok(self.tokens)
225 }
226
227 fn lex_hash(&mut self) {
229 if self.peek(1) == Some('!') {
230 let start = self.here(2);
231 self.advance();
232 self.advance();
233 let mut text = String::new();
234 while self.pos < self.chars.len() {
235 if self.chars[self.pos] == '\\' && self.skip_line_continuation() {
236 text.push('\n');
237 continue;
238 }
239 if self.chars[self.pos] == '\n' {
240 break;
241 }
242 text.push(self.chars[self.pos]);
243 self.advance();
244 }
245 self.push_token(TokenKind::Directive, text, start);
246 } else {
247 while self.pos < self.chars.len() && self.chars[self.pos] != '\n' {
248 self.advance();
249 }
250 }
251 }
252
253 fn skip_block_comment(&mut self) -> OpyResult<()> {
254 let start = self.here(2);
255 self.advance();
256 self.advance();
257 while self.pos < self.chars.len() {
258 if self.chars[self.pos] == '*' && self.peek(1) == Some('/') {
259 self.advance();
260 self.advance();
261 return Ok(());
262 }
263 if self.chars[self.pos] == '\n' {
264 self.advance();
265 self.line += 1;
266 self.col = 1;
267 } else {
268 self.advance();
269 }
270 }
271 Err(OpyError::at(
272 "lex-error",
273 "unterminated block comment",
274 start,
275 ))
276 }
277
278 fn lex_string(&mut self, quote: char) -> OpyResult<()> {
279 let start = self.here(1);
280 self.advance();
281 let mut value = String::new();
282 let mut raw = String::new();
283 while self.pos < self.chars.len() {
284 let ch = self.chars[self.pos];
285 if ch == quote {
286 self.advance();
287 let mut token = self.make_token(TokenKind::String, value, start);
288 token.raw = Some(raw);
289 self.tokens.push(token);
290 return Ok(());
291 }
292 if ch == '\\' {
293 let escape_start = self.here(1);
294 raw.push(ch);
295 self.advance();
296 if self.pos >= self.chars.len() {
297 break;
298 }
299 let escaped = self.chars[self.pos];
300 raw.push(escaped);
301 if escaped == '&' {
302 self.advance();
303 let mut entity_name = String::new();
304 while let Some(character) = self.chars.get(self.pos).copied() {
305 if character == ';' {
306 break;
307 }
308 if !(character.is_ascii_alphanumeric() || character == '_') {
309 return Err(OpyError::at(
310 "invalid-string-entity",
311 format!("invalid character '{character}' in string entity"),
312 Span::new(self.file_id, escape_start.start, self.here(1).end),
313 ));
314 }
315 entity_name.push(character);
316 raw.push(character);
317 self.advance();
318 }
319 if self.chars.get(self.pos) != Some(&';') {
320 return Err(OpyError::at(
321 "invalid-string-entity",
322 "expected ';' to terminate string entity",
323 Span::new(self.file_id, escape_start.start, self.here(0).start),
324 ));
325 }
326 raw.push(';');
327 self.advance();
328 let Some(codepoint) = crate::string_entities::codepoint(&entity_name) else {
329 return Err(OpyError::at(
330 "unknown-string-entity",
331 format!("unknown string entity '{entity_name}'"),
332 Span::new(self.file_id, escape_start.start, self.here(0).start),
333 ));
334 };
335 value.push(codepoint);
336 continue;
337 }
338 if escaped == 'u' {
339 self.advance();
340 let mut codepoint = 0_u32;
341 for _ in 0..4 {
342 let Some(digit) = self.chars.get(self.pos).copied() else {
343 return Err(OpyError::at(
344 "lex-error",
345 "Unicode escape requires four hexadecimal digits",
346 Span::new(self.file_id, escape_start.start, self.here(0).start),
347 ));
348 };
349 let Some(digit_value) = digit.to_digit(16) else {
350 return Err(OpyError::at(
351 "lex-error",
352 "Unicode escape requires four hexadecimal digits",
353 Span::new(self.file_id, escape_start.start, self.here(1).end),
354 ));
355 };
356 raw.push(digit);
357 codepoint = codepoint * 16 + digit_value;
358 self.advance();
359 }
360 let Some(decoded) = char::from_u32(codepoint) else {
361 return Err(OpyError::at(
362 "lex-error",
363 "Unicode escape does not name a Unicode scalar value",
364 Span::new(self.file_id, escape_start.start, self.here(0).start),
365 ));
366 };
367 value.push(decoded);
368 continue;
369 }
370 value.push(decode_string_escape(escaped));
371 self.advance();
372 continue;
373 }
374 if ch == '\n' {
375 return Err(OpyError::at(
376 "lex-error",
377 "unterminated string literal",
378 start,
379 ));
380 }
381 raw.push(ch);
382 value.push(ch);
383 self.advance();
384 }
385 Err(OpyError::at(
386 "lex-error",
387 "unterminated string literal",
388 start,
389 ))
390 }
391
392 fn skip_line_continuation(&mut self) -> bool {
393 let mut offset = 1;
394 while matches!(self.peek(offset), Some(' ' | '\r')) {
395 offset += 1;
396 }
397 if self.peek(offset) != Some('\n') {
398 return false;
399 }
400 for _ in 0..=offset {
401 self.advance();
402 }
403 self.line += 1;
404 self.col = 1;
405 true
406 }
407
408 fn lex_number(&mut self) -> OpyResult<()> {
409 let start = self.here(1);
410 let mut text = String::new();
411 if self.chars[self.pos] == '0'
412 && matches!(self.peek(1), Some('x' | 'X' | 'b' | 'B' | 'o' | 'O'))
413 {
414 text.push('0');
415 self.advance();
416 let letter = self.chars[self.pos];
417 text.push(letter);
418 self.advance();
419 let radix = match letter {
420 'x' | 'X' => 16,
421 'o' | 'O' => 8,
422 _ => 2,
423 };
424 let digits_start = self.pos;
425 while self.pos < self.chars.len() && self.chars[self.pos].is_digit(radix) {
426 text.push(self.chars[self.pos]);
427 self.advance();
428 }
429 if self.pos == digits_start {
430 return Err(OpyError::at(
431 "lex-error",
432 "integer radix literal requires at least one digit",
433 Span::new(self.file_id, start.start, self.here(0).start),
434 ));
435 }
436 self.push_token(TokenKind::Number, text, start);
437 return Ok(());
438 }
439 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
440 text.push(self.chars[self.pos]);
441 self.advance();
442 }
443 if self.pos < self.chars.len()
444 && self.chars[self.pos] == '.'
445 && self.peek(1).is_some_and(|c| c.is_ascii_digit())
446 {
447 text.push('.');
448 self.advance();
449 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
450 text.push(self.chars[self.pos]);
451 self.advance();
452 }
453 }
454 if self.pos < self.chars.len()
457 && (self.chars[self.pos] == 'e' || self.chars[self.pos] == 'E')
458 {
459 let mut lookahead = self.pos + 1;
460 if lookahead < self.chars.len()
461 && (self.chars[lookahead] == '+' || self.chars[lookahead] == '-')
462 {
463 lookahead += 1;
464 }
465 if lookahead < self.chars.len() && self.chars[lookahead].is_ascii_digit() {
466 text.push('e');
467 self.advance();
468 if self.pos < self.chars.len()
469 && (self.chars[self.pos] == '+' || self.chars[self.pos] == '-')
470 {
471 text.push(self.chars[self.pos]);
472 self.advance();
473 }
474 while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() {
475 text.push(self.chars[self.pos]);
476 self.advance();
477 }
478 }
479 }
480 self.push_token(TokenKind::Number, text, start);
481 Ok(())
482 }
483
484 fn lex_ident(&mut self) {
485 let start = self.here(1);
486 let mut text = String::new();
487 while self.pos < self.chars.len() && is_ident_continue(self.chars[self.pos]) {
488 text.push(self.chars[self.pos]);
489 self.advance();
490 }
491 self.push_token(TokenKind::Ident, text, start);
492 }
493
494 fn single(&mut self, kind: TokenKind) {
495 let start = self.here(1);
496 let text = self.chars[self.pos].to_string();
497 self.advance();
498 self.push_token(kind, text, start);
499 }
500
501 fn lex_two(&mut self, plain: TokenKind, assign: TokenKind, second: char) {
503 let start = self.here(1);
504 let (kind, text) = if self.peek(1) == Some(second) {
505 self.advance();
506 let text = format!("{}{}", self.chars[self.pos - 1], second);
507 self.advance();
508 (assign, text)
509 } else {
510 let text = self.chars[self.pos].to_string();
511 self.advance();
512 (plain, text)
513 };
514 self.push_token(kind, text, start);
515 }
516
517 fn lex_duplicate(&mut self, kind: TokenKind, text: &str) {
518 let start = self.here(1);
519 self.advance();
520 self.advance();
521 self.push_token(kind, text, start);
522 }
523
524 fn make_token(&self, kind: TokenKind, text: impl Into<String>, start: Span) -> Token {
525 Token::new(
526 kind,
527 text,
528 Span::new(self.file_id, start.start, self.here(0).start),
529 )
530 }
531
532 fn push_token(&mut self, kind: TokenKind, text: impl Into<String>, start: Span) {
533 let token = self.make_token(kind, text, start);
534 self.tokens.push(token);
535 }
536
537 fn here(&self, width: usize) -> Span {
538 Span::new(
539 self.file_id,
540 Position::new(self.line, self.col),
541 Position::new(self.line, self.col + width as u32),
542 )
543 }
544
545 fn peek(&self, offset: usize) -> Option<char> {
546 self.chars.get(self.pos + offset).copied()
547 }
548
549 fn advance(&mut self) {
550 self.pos += 1;
551 self.col += 1;
552 }
553}
554
555pub fn is_identifier(text: &str) -> bool {
558 let mut characters = text.chars();
559 characters.next().is_some_and(is_ident_start) && characters.all(is_ident_continue)
560}
561
562pub(crate) fn decode_string_escape(character: char) -> char {
563 match character {
564 'n' => '\n',
565 't' => '\t',
566 'r' => '\r',
567 '\\' => '\\',
568 '"' => '"',
569 '\'' => '\'',
570 other => other,
571 }
572}
573
574pub fn is_ident_start(c: char) -> bool {
576 c.is_ascii_alphabetic() || c == '_'
577}
578
579pub fn is_ident_continue(c: char) -> bool {
581 c.is_ascii_alphanumeric() || c == '_'
582}
583
584#[cfg(test)]
585mod tests {
586 use super::*;
587
588 fn lex_ok(text: &str) -> Vec<Token> {
589 lex(LexInput { file_id: 0, text }).unwrap()
590 }
591
592 #[test]
593 fn lexes_basic_rule() {
594 let tokens = lex_ok("rule \"setup\":\n @Event global\n disableInspector()\n");
595 let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind).collect();
596 assert!(kinds.contains(&TokenKind::Ident));
597 assert!(kinds.contains(&TokenKind::String));
598 assert!(kinds.contains(&TokenKind::Colon));
599 assert!(kinds.contains(&TokenKind::At));
600 assert!(kinds.contains(&TokenKind::LParen));
601 assert!(kinds.contains(&TokenKind::Eof));
602 }
603
604 #[test]
605 fn numbers_preserve_text() {
606 let tokens = lex_ok("1 2.5 0.016 100");
607 let numbers: Vec<&str> = tokens
608 .iter()
609 .filter(|t| t.kind == TokenKind::Number)
610 .map(|t| t.text.as_str())
611 .collect();
612 assert_eq!(numbers, vec!["1", "2.5", "0.016", "100"]);
613 }
614
615 #[test]
616 fn radix_literals_lex_as_single_numbers() {
617 let tokens = lex_ok("0x1F 0b101 0o17 0X1f 0B101 0O17");
621 let numbers: Vec<&str> = tokens
622 .iter()
623 .filter(|t| t.kind == TokenKind::Number)
624 .map(|t| t.text.as_str())
625 .collect();
626 assert_eq!(
627 numbers,
628 vec!["0x1F", "0b101", "0o17", "0X1f", "0B101", "0O17"]
629 );
630 }
631
632 #[test]
633 fn radix_literal_without_digits_is_an_error() {
634 for text in ["0x", "0b", "0o"] {
635 let error = lex(LexInput { file_id: 0, text }).unwrap_err();
636 assert_eq!(error.code, "lex-error");
637 }
638 }
639
640 #[test]
641 fn directives_and_comments() {
642 let tokens = lex_ok("#!define X 1\n# comment\nrule \"r\":\n");
643 let directive = tokens
644 .iter()
645 .find(|t| t.kind == TokenKind::Directive)
646 .unwrap();
647 assert_eq!(directive.text, "define X 1");
648 assert!(!tokens.iter().any(|t| t.text == "comment"));
649 }
650
651 #[test]
652 fn directive_line_continuation_is_part_of_one_directive() {
653 let tokens = lex_ok("#!define X first + \\\n second\n");
654 let directive = tokens
655 .iter()
656 .find(|token| token.kind == TokenKind::Directive)
657 .unwrap();
658 assert_eq!(directive.text, "define X first + \n second");
659 assert_eq!(directive.span.start, Position::new(1, 1));
660 assert_eq!(directive.span.end, Position::new(2, 9));
661 }
662
663 #[test]
664 fn operators() {
665 let tokens = lex_ok("a += b == c <= d != e / f");
666 let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind).collect();
667 for expected in [
668 TokenKind::PlusAssign,
669 TokenKind::Eq,
670 TokenKind::Le,
671 TokenKind::Ne,
672 TokenKind::Slash,
673 ] {
674 assert!(
675 kinds.contains(&expected),
676 "missing {expected:?} in {kinds:?}"
677 );
678 }
679 }
680
681 #[test]
682 fn power_operators_disambiguate() {
683 let tokens = lex_ok("a **= b ** c *= d");
686 let kinds: Vec<TokenKind> = tokens.iter().map(|t| t.kind).collect();
687 assert_eq!(
688 kinds,
689 vec![
690 TokenKind::Ident,
691 TokenKind::DoubleStarAssign,
692 TokenKind::Ident,
693 TokenKind::DoubleStar,
694 TokenKind::Ident,
695 TokenKind::StarAssign,
696 TokenKind::Ident,
697 TokenKind::Eof,
698 ]
699 );
700 let assign = tokens
701 .iter()
702 .find(|t| t.kind == TokenKind::DoubleStarAssign)
703 .unwrap();
704 assert_eq!(assign.text, "**=");
705 }
706
707 #[test]
708 fn postfix_operators_are_single_tokens() {
709 let tokens = lex_ok("counter++ points--");
710 assert_eq!(
711 tokens.iter().map(|token| token.kind).collect::<Vec<_>>(),
712 vec![
713 TokenKind::Ident,
714 TokenKind::Increment,
715 TokenKind::Ident,
716 TokenKind::Decrement,
717 TokenKind::Eof,
718 ]
719 );
720 assert_eq!(tokens[1].span.start.col, 8);
721 assert_eq!(tokens[1].span.end.col, 10);
722 }
723
724 #[test]
725 fn unterminated_string_is_structured() {
726 let error = lex(LexInput {
727 file_id: 0,
728 text: "rule \"x\n",
729 })
730 .unwrap_err();
731 assert_eq!(error.code, "lex-error");
732 assert!(error.span.is_some());
733 }
734
735 #[test]
736 fn unicode_escapes_decode_at_the_string_boundary_and_keep_source_span() {
737 let tokens = lex_ok(r#"rule "pa\ufeffssed":"#);
741 let string = tokens
742 .iter()
743 .find(|token| token.kind == TokenKind::String)
744 .expect("rule name string token");
745 assert_eq!(string.text, "pa\u{feff}ssed");
746 assert_eq!(string.raw.as_deref(), Some(r"pa\ufeffssed"));
747 assert_eq!(string.span.start, Position::new(1, 6));
748 assert_eq!(string.span.end, Position::new(1, 20));
749 }
750
751 #[test]
752 fn existing_string_escapes_keep_their_decoded_values() {
753 let source = concat!("\"", "\\n", "\\t", "\\r", "\\\"", "\\\\", "\"");
754 let string = lex_ok(source)
755 .into_iter()
756 .find(|token| token.kind == TokenKind::String)
757 .expect("string token");
758 assert_eq!(string.text, "\n\t\r\"\\");
759 }
760
761 #[test]
762 fn named_string_entities_decode_with_raw_provenance() {
763 let string = lex_ok(r#""a\&black_square;b\&fullwidth_space;c""#)
764 .into_iter()
765 .find(|token| token.kind == TokenKind::String)
766 .expect("string token");
767 assert_eq!(string.text, "a■b c");
768 assert_eq!(
769 string.raw.as_deref(),
770 Some(r"a\&black_square;b\&fullwidth_space;c")
771 );
772 }
773
774 #[test]
775 fn invalid_string_entities_are_structured_lex_errors() {
776 for (source, code) in [
777 (r#""\&missing;""#, "unknown-string-entity"),
778 (r#""\&black-square;""#, "invalid-string-entity"),
779 (r#""\&black_square""#, "invalid-string-entity"),
780 ] {
781 let error = lex(LexInput {
782 file_id: 0,
783 text: source,
784 })
785 .expect_err("invalid string entity unexpectedly lexed");
786 assert_eq!(error.code, code);
787 assert_eq!(error.span.unwrap().start, Position::new(1, 2));
788 }
789 }
790
791 #[test]
792 fn malformed_unicode_escapes_are_lex_errors() {
793 for source in [r#""\u"#, r#""\u12G4"#, r#""\u{1F600}"#] {
794 let error = lex(LexInput {
795 file_id: 0,
796 text: source,
797 })
798 .expect_err("malformed Unicode escape unexpectedly lexed");
799 assert_eq!(error.code, "lex-error");
800 assert!(error.message.contains("Unicode escape"));
801 assert_eq!(error.span.unwrap().start, Position::new(1, 2));
802 }
803 }
804
805 #[test]
806 fn backslash_line_continuation_is_not_a_token() {
807 let tokens = lex_ok("one \\\ntwo");
808 assert_eq!(
809 tokens.iter().map(|token| token.kind).collect::<Vec<_>>(),
810 vec![TokenKind::Ident, TokenKind::Ident, TokenKind::Eof]
811 );
812 assert_eq!(tokens[1].span.start.line, 2);
813 assert_eq!(tokens[1].span.start.col, 1);
814 }
815
816 #[test]
817 fn crlf_line_continuation_tracks_the_next_line() {
818 let tokens = lex_ok("one \\\r\ntwo");
819 assert_eq!(tokens[1].span.start, Position::new(2, 1));
820 }
821
822 #[test]
823 fn whitespace_before_line_ending_is_part_of_the_continuation() {
824 let tokens = lex_ok("one \\ \ntwo");
825 assert_eq!(tokens[1].span.start, Position::new(2, 1));
826 }
827
828 #[test]
829 fn non_newline_backslash_remains_a_lex_error() {
830 for text in ["one \\ two", "one \\", "one \\ \t\ntwo"] {
831 let error = lex(LexInput { file_id: 0, text }).unwrap_err();
832 assert_eq!(error.code, "lex-error");
833 assert_eq!(error.message, "unexpected character '\\'");
834 assert_eq!(error.span.unwrap().start, Position::new(1, 5));
835 }
836 }
837}