1use crate::token::*;
2use std::fmt;
3
4#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum LexerError {
7 UnexpectedCharacter(char, Span),
8 UnterminatedString(Span),
9 UnterminatedBlockComment(Span),
10 IntegerLiteralOutOfRange(String, Span),
13}
14
15impl fmt::Display for LexerError {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 match self {
18 LexerError::UnexpectedCharacter(ch, span) => {
19 write!(f, "Unexpected character '{ch}' at {span}")
20 }
21 LexerError::UnterminatedString(span) => {
22 write!(f, "Unterminated string at {span}")
23 }
24 LexerError::UnterminatedBlockComment(span) => {
25 write!(f, "Unterminated block comment at {span}")
26 }
27 LexerError::IntegerLiteralOutOfRange(lit, span) => {
28 write!(
29 f,
30 "Integer literal `{lit}` is out of range for int (i64) at {span}"
31 )
32 }
33 }
34 }
35}
36
37impl std::error::Error for LexerError {}
38
39pub struct Lexer {
41 source: Vec<char>,
42 pos: usize,
43 byte_pos: usize,
44 line: usize,
45 column: usize,
46}
47
48impl Lexer {
49 pub fn new(source: &str) -> Self {
50 Self {
51 source: source.chars().collect(),
52 pos: 0,
53 byte_pos: 0,
54 line: 1,
55 column: 1,
56 }
57 }
58
59 pub fn with_position(source: &str, line: usize, column: usize) -> Self {
62 Self {
63 source: source.chars().collect(),
64 pos: 0,
65 byte_pos: 0,
66 line,
67 column,
68 }
69 }
70
71 pub fn tokenize_with_comments(&mut self) -> Result<Vec<Token>, LexerError> {
73 self.tokenize_inner(true)
74 }
75
76 pub fn tokenize(&mut self) -> Result<Vec<Token>, LexerError> {
77 self.tokenize_inner(false)
78 }
79
80 fn tokenize_inner(&mut self, keep_comments: bool) -> Result<Vec<Token>, LexerError> {
81 let mut tokens = Vec::new();
82
83 if self.pos == 0 && self.source.starts_with(&['#', '!']) {
86 while self.pos < self.source.len() && self.source[self.pos] != '\n' {
87 self.advance();
88 }
89 }
90
91 while self.pos < self.source.len() {
92 let ch = self.source[self.pos];
93
94 if ch == ' ' || ch == '\t' || ch == '\r' {
95 self.advance();
96 continue;
97 }
98
99 if ch == '\\' && self.peek() == Some('\n') {
101 self.advance();
102 self.advance();
103 self.line += 1;
104 self.column = 1;
105 continue;
106 }
107
108 if ch == '\n' {
109 let start = self.byte_pos;
110 tokens.push(Token::with_span(
111 TokenKind::Newline,
112 Span::with_offsets(start, start + 1, self.line, self.column),
113 ));
114 self.advance();
115 self.line += 1;
116 self.column = 1;
117 continue;
118 }
119
120 if ch == '/' {
121 if self.peek() == Some('/') {
122 let tok = self.read_line_comment();
123 if keep_comments {
124 tokens.push(tok);
125 }
126 continue;
127 }
128 if self.peek() == Some('*') {
129 let tok = self.read_block_comment()?;
130 if keep_comments {
131 tokens.push(tok);
132 }
133 continue;
134 }
135 }
136
137 if ch == 'r' && self.peek() == Some('"') {
138 tokens.push(self.read_raw_string()?);
139 continue;
140 }
141
142 if ch == 'r' && self.peek() == Some('#') {
148 if let Some(hashes) = self.raw_hash_count() {
149 tokens.push(self.read_raw_string_hashed(hashes)?);
150 continue;
151 }
152 }
153
154 if ch == '"' {
155 tokens.push(self.read_string()?);
156 continue;
157 }
158
159 if ch.is_ascii_digit() {
160 tokens.push(self.read_number()?);
161 continue;
162 }
163
164 if ch.is_alphabetic() || ch == '_' {
165 tokens.push(self.read_identifier());
166 continue;
167 }
168
169 if let Some(tok) = self.try_two_char_op() {
170 tokens.push(tok);
171 continue;
172 }
173
174 if let Some(kind) = self.single_char_token(ch) {
175 let start = self.byte_pos;
176 let col = self.column;
177 self.advance();
178 tokens.push(Token::with_span(
179 kind,
180 Span::with_offsets(start, self.byte_pos, self.line, col),
181 ));
182 continue;
183 }
184
185 return Err(LexerError::UnexpectedCharacter(
186 ch,
187 Span::with_offsets(
188 self.byte_pos,
189 self.byte_pos + ch.len_utf8(),
190 self.line,
191 self.column,
192 ),
193 ));
194 }
195
196 tokens.push(self.token(TokenKind::Eof));
197 Ok(tokens)
198 }
199
200 fn peek(&self) -> Option<char> {
201 self.source.get(self.pos + 1).copied()
202 }
203
204 fn advance(&mut self) {
205 if self.pos < self.source.len() {
206 self.byte_pos += self.source[self.pos].len_utf8();
207 }
208 self.pos += 1;
209 self.column += 1;
210 }
211
212 fn token(&self, kind: TokenKind) -> Token {
213 Token::with_span(
214 kind,
215 Span::with_offsets(self.byte_pos, self.byte_pos, self.line, self.column),
216 )
217 }
218
219 fn read_line_comment(&mut self) -> Token {
220 let start_byte = self.byte_pos;
221 let start_col = self.column;
222 let start_line = self.line;
223 self.advance();
224 self.advance();
225 let is_doc = self.source.get(self.pos).copied() == Some('/')
227 && self.source.get(self.pos + 1).copied() != Some('/');
228 if is_doc {
229 self.advance();
230 }
231 let mut text = String::new();
232 while self.pos < self.source.len() && self.source[self.pos] != '\n' {
233 text.push(self.source[self.pos]);
234 self.advance();
235 }
236 Token::with_span(
237 TokenKind::LineComment { text, is_doc },
238 Span::with_offsets(start_byte, self.byte_pos, start_line, start_col),
239 )
240 }
241
242 fn read_block_comment(&mut self) -> Result<Token, LexerError> {
243 let start_byte = self.byte_pos;
244 let start = Span::with_offsets(self.byte_pos, self.byte_pos, self.line, self.column);
245 self.advance();
246 self.advance();
247 let is_doc = self.source.get(self.pos).copied() == Some('*')
249 && self.source.get(self.pos + 1).copied() != Some('*')
250 && self.source.get(self.pos + 1).copied() != Some('/');
251 if is_doc {
252 self.advance();
253 }
254 let mut text = String::new();
255 let mut depth = 1;
256 while self.pos < self.source.len() && depth > 0 {
257 if self.source[self.pos] == '/' && self.peek() == Some('*') {
258 depth += 1;
259 text.push('/');
260 text.push('*');
261 self.advance();
262 self.advance();
263 } else if self.source[self.pos] == '*' && self.peek() == Some('/') {
264 depth -= 1;
265 if depth > 0 {
266 text.push('*');
267 text.push('/');
268 }
269 self.advance();
270 self.advance();
271 } else if self.source[self.pos] == '\n' {
272 text.push('\n');
273 self.byte_pos += self.source[self.pos].len_utf8();
274 self.line += 1;
275 self.column = 1;
276 self.pos += 1;
277 } else {
278 text.push(self.source[self.pos]);
279 self.advance();
280 }
281 }
282 if depth > 0 {
283 return Err(LexerError::UnterminatedBlockComment(start));
284 }
285 let mut span = Span::with_offsets(start_byte, self.byte_pos, start.line, start.column);
292 span.end_line = self.line;
293 Ok(Token::with_span(
294 TokenKind::BlockComment { text, is_doc },
295 span,
296 ))
297 }
298
299 fn capture_interpolation_expr(
313 &mut self,
314 start: Span,
315 ) -> Result<(String, usize, usize), LexerError> {
316 self.advance(); self.advance(); let expr_line = self.line;
319 let expr_col = self.column;
320 let mut depth = 1usize;
321 let mut expr = String::new();
322 let mut in_string = false;
323 while self.pos < self.source.len() && depth > 0 {
324 let ch = self.source[self.pos];
325 if in_string {
326 if ch == '\\' {
327 expr.push(ch);
330 self.advance();
331 if self.pos >= self.source.len() {
332 return Err(LexerError::UnterminatedString(start));
333 }
334 let escaped = self.source[self.pos];
335 if escaped == '\n' {
336 self.line += 1;
337 self.column = 0; }
339 expr.push(escaped);
340 self.advance();
341 continue;
342 }
343 if ch == '"' {
344 in_string = false;
345 }
346 } else {
347 match ch {
348 '\\' => {
355 return Err(LexerError::UnexpectedCharacter(
356 '\\',
357 Span::with_offsets(
358 self.byte_pos,
359 self.byte_pos + 1,
360 self.line,
361 self.column,
362 ),
363 ));
364 }
365 '"' => in_string = true,
366 '{' => depth += 1,
367 '}' => {
368 depth -= 1;
369 if depth == 0 {
370 break;
371 }
372 }
373 _ => {}
374 }
375 }
376 if ch == '\n' {
377 self.line += 1;
378 self.column = 0; }
380 expr.push(ch);
381 self.advance();
382 }
383 if self.pos >= self.source.len() {
384 return Err(LexerError::UnterminatedString(start));
385 }
386 if expr.trim().is_empty() {
387 return Err(LexerError::UnexpectedCharacter(
388 '}',
389 Span::with_offsets(self.byte_pos, self.byte_pos + 1, self.line, self.column),
390 ));
391 }
392 self.advance(); Ok((expr, expr_line, expr_col))
394 }
395
396 fn read_string(&mut self) -> Result<Token, LexerError> {
397 let start_byte = self.byte_pos;
398 let start = Span::with_offsets(start_byte, start_byte, self.line, self.column);
399
400 if self.pos + 2 < self.source.len()
401 && self.source[self.pos + 1] == '"'
402 && self.source[self.pos + 2] == '"'
403 {
404 return self.read_multi_line_string(start_byte, start);
405 }
406
407 self.advance();
408
409 let mut value = String::new();
410 let mut segments: Vec<StringSegment> = Vec::new();
411 let mut has_interpolation = false;
412
413 while self.pos < self.source.len() {
414 let ch = self.source[self.pos];
415 if ch == '"' {
416 self.advance();
417 if has_interpolation {
418 if !value.is_empty() {
419 segments.push(StringSegment::Literal(value));
420 }
421 return Ok(Token::with_span(
422 TokenKind::InterpolatedString(segments),
423 Span::with_offsets(start_byte, self.byte_pos, start.line, start.column),
424 ));
425 }
426 return Ok(Token::with_span(
427 TokenKind::StringLiteral(value),
428 Span::with_offsets(start_byte, self.byte_pos, start.line, start.column),
429 ));
430 }
431
432 if ch == '$' && self.peek() == Some('{') {
433 has_interpolation = true;
434 if !value.is_empty() {
435 segments.push(StringSegment::Literal(std::mem::take(&mut value)));
436 }
437 let (expr, expr_line, expr_col) = self.capture_interpolation_expr(start)?;
438 segments.push(StringSegment::Expression(expr, expr_line, expr_col));
439 continue;
440 }
441
442 if ch == '\\' {
443 self.advance();
444 if self.pos >= self.source.len() {
445 return Err(LexerError::UnterminatedString(start));
446 }
447 let escaped = self.source[self.pos];
448 match escaped {
449 'n' => value.push('\n'),
450 'r' => value.push('\r'),
451 't' => value.push('\t'),
452 '0' => value.push('\0'),
453 '\\' => value.push('\\'),
454 '"' => value.push('"'),
455 '$' => value.push('$'),
456 _ => {
457 value.push('\\');
458 value.push(escaped);
459 }
460 }
461 self.advance();
462 continue;
463 }
464
465 if ch == '\n' {
466 return Err(LexerError::UnterminatedString(start));
467 }
468
469 value.push(ch);
470 self.advance();
471 }
472 Err(LexerError::UnterminatedString(start))
473 }
474
475 fn read_multi_line_string(
476 &mut self,
477 start_byte: usize,
478 start: Span,
479 ) -> Result<Token, LexerError> {
480 self.advance();
481 self.advance();
482 self.advance();
483
484 if self.pos < self.source.len() && self.source[self.pos] == '\n' {
485 self.advance();
486 self.line += 1;
487 self.column = 1;
488 }
489
490 let mut value = String::new();
491 let mut segments: Vec<StringSegment> = Vec::new();
492 let mut has_interpolation = false;
493
494 while self.pos < self.source.len() {
495 if self.source[self.pos] == '"'
496 && self.pos + 2 < self.source.len()
497 && self.source[self.pos + 1] == '"'
498 && self.source[self.pos + 2] == '"'
499 {
500 self.advance();
501 self.advance();
502 self.advance();
503 if has_interpolation {
504 if !value.is_empty() {
505 segments.push(StringSegment::Literal(std::mem::take(&mut value)));
506 }
507 let full_text: String = segments
510 .iter()
511 .map(|seg| match seg {
512 StringSegment::Literal(s) => s.as_str(),
513 _ => "",
514 })
515 .collect();
516 let indent = common_indent(&full_text);
517 let segments = if indent > 0 {
518 let stripped_segments = segments
519 .into_iter()
520 .map(|seg| match seg {
521 StringSegment::Literal(s) => {
522 StringSegment::Literal(strip_indent(&s, indent))
523 }
524 other => other,
525 })
526 .collect();
527 strip_trailing_newline_segments(stripped_segments)
528 } else {
529 strip_trailing_newline_segments(segments)
530 };
531 let mut span =
532 Span::with_offsets(start_byte, self.byte_pos, start.line, start.column);
533 span.end_line = self.line;
534 return Ok(Token::with_span(
535 TokenKind::InterpolatedString(segments),
536 span,
537 ));
538 }
539 let stripped = strip_common_indent(&value);
540 let mut span =
541 Span::with_offsets(start_byte, self.byte_pos, start.line, start.column);
542 span.end_line = self.line;
543 return Ok(Token::with_span(TokenKind::StringLiteral(stripped), span));
544 }
545
546 if self.source[self.pos] == '$' && self.peek() == Some('{') {
547 has_interpolation = true;
548 if !value.is_empty() {
549 segments.push(StringSegment::Literal(std::mem::take(&mut value)));
550 }
551 let (expr, expr_line, expr_col) = self.capture_interpolation_expr(start)?;
552 segments.push(StringSegment::Expression(expr, expr_line, expr_col));
553 continue;
554 }
555
556 if self.source[self.pos] == '\\'
557 && self.peek() == Some('$')
558 && self.source.get(self.pos + 2) == Some(&'{')
559 {
560 let preceding_backslashes =
562 value.chars().rev().take_while(|ch| *ch == '\\').count();
563 if preceding_backslashes % 2 == 0 {
564 self.advance();
565 value.push('$');
566 self.advance();
567 continue;
568 }
569 }
570
571 if self.source[self.pos] == '\n' {
572 value.push('\n');
573 self.advance();
574 self.line += 1;
575 self.column = 1;
576 } else {
577 value.push(self.source[self.pos]);
578 self.advance();
579 }
580 }
581 Err(LexerError::UnterminatedString(start))
582 }
583
584 fn raw_hash_count(&self) -> Option<usize> {
589 let mut k = 1; let mut hashes = 0;
591 while self.source.get(self.pos + k) == Some(&'#') {
592 hashes += 1;
593 k += 1;
594 }
595 if hashes >= 1 && self.source.get(self.pos + k) == Some(&'"') {
596 Some(hashes)
597 } else {
598 None
599 }
600 }
601
602 fn read_raw_string_hashed(&mut self, hashes: usize) -> Result<Token, LexerError> {
609 let start_byte = self.byte_pos;
610 let start = Span::with_offsets(start_byte, start_byte, self.line, self.column);
611 self.advance(); for _ in 0..hashes {
614 self.advance();
615 }
616 self.advance(); let mut value = String::new();
619 while self.pos < self.source.len() {
620 let ch = self.source[self.pos];
621 if ch == '"' {
622 let closed = (1..=hashes).all(|k| self.source.get(self.pos + k) == Some(&'#'));
625 if closed {
626 self.advance(); for _ in 0..hashes {
628 self.advance();
629 }
630 return Ok(Token::with_span(
631 TokenKind::RawStringLiteral(value),
632 Span::with_offsets(start_byte, self.byte_pos, start.line, start.column),
633 ));
634 }
635 }
636 if ch == '\n' {
637 return Err(LexerError::UnterminatedString(start));
638 }
639 value.push(ch);
640 self.advance();
641 }
642 Err(LexerError::UnterminatedString(start))
643 }
644
645 fn read_raw_string(&mut self) -> Result<Token, LexerError> {
647 let start_byte = self.byte_pos;
648 let start = Span::with_offsets(start_byte, start_byte, self.line, self.column);
649 self.advance();
650 self.advance();
651
652 let mut value = String::new();
653 while self.pos < self.source.len() {
654 let ch = self.source[self.pos];
655 if ch == '"' {
656 self.advance();
657 return Ok(Token::with_span(
658 TokenKind::RawStringLiteral(value),
659 Span::with_offsets(start_byte, self.byte_pos, start.line, start.column),
660 ));
661 }
662 if ch == '\n' {
663 return Err(LexerError::UnterminatedString(start));
664 }
665 value.push(ch);
666 self.advance();
667 }
668 Err(LexerError::UnterminatedString(start))
669 }
670
671 fn read_number(&mut self) -> Result<Token, LexerError> {
672 let start_byte = self.byte_pos;
673 let start_col = self.column;
674 let mut num_str = String::new();
675 let mut is_float = false;
676
677 while self.pos < self.source.len()
678 && (self.source[self.pos].is_ascii_digit() || self.source[self.pos] == '.')
679 {
680 if self.source[self.pos] == '.' {
681 if is_float {
682 break;
683 }
684 if let Some(next) = self.source.get(self.pos + 1) {
686 if !next.is_ascii_digit() {
687 break;
688 }
689 } else {
690 break;
691 }
692 is_float = true;
693 }
694 num_str.push(self.source[self.pos]);
695 self.advance();
696 }
697
698 if !is_float {
699 if let Some(ms) = self.try_duration_suffix(&num_str) {
700 return Ok(Token::with_span(
701 TokenKind::DurationLiteral(ms),
702 Span::with_offsets(start_byte, self.byte_pos, self.line, start_col),
703 ));
704 }
705 }
706
707 let span = Span::with_offsets(start_byte, self.byte_pos, self.line, start_col);
708 if is_float {
709 let n: f64 = num_str.parse().unwrap_or(0.0);
710 Ok(Token::with_span(TokenKind::FloatLiteral(n), span))
711 } else {
712 match num_str.parse::<i64>() {
713 Ok(n) => Ok(Token::with_span(TokenKind::IntLiteral(n), span)),
714 Err(_) => {
715 Err(LexerError::IntegerLiteralOutOfRange(num_str, span))
723 }
724 }
725 }
726 }
727
728 fn try_duration_suffix(&mut self, num_str: &str) -> Option<u64> {
730 let n: u64 = num_str.parse().ok()?;
731 if self.pos < self.source.len() {
732 let ch = self.source[self.pos];
733 if ch == 'm'
734 && self.source.get(self.pos + 1) == Some(&'s')
735 && is_duration_suffix_boundary(self.source.get(self.pos + 2))
736 {
737 self.advance();
738 self.advance();
739 return Some(n);
740 }
741 if ch == 's' && is_duration_suffix_boundary(self.source.get(self.pos + 1)) {
742 self.advance();
743 return Some(n.saturating_mul(1000));
744 }
745 if ch == 'm' && is_duration_suffix_boundary(self.source.get(self.pos + 1)) {
746 self.advance();
747 return Some(n.saturating_mul(60_000));
748 }
749 if ch == 'h' && is_duration_suffix_boundary(self.source.get(self.pos + 1)) {
750 self.advance();
751 return Some(n.saturating_mul(3_600_000));
752 }
753 if ch == 'd' && is_duration_suffix_boundary(self.source.get(self.pos + 1)) {
754 self.advance();
755 return Some(n.saturating_mul(86_400_000));
756 }
757 if ch == 'w' && is_duration_suffix_boundary(self.source.get(self.pos + 1)) {
758 self.advance();
759 return Some(n.saturating_mul(604_800_000));
760 }
761 }
762 None
763 }
764
765 fn read_identifier(&mut self) -> Token {
766 let start_byte = self.byte_pos;
767 let start_col = self.column;
768 let mut ident = String::new();
769
770 while self.pos < self.source.len() {
771 let ch = self.source[self.pos];
772 if ch.is_alphanumeric() || ch == '_' {
773 ident.push(ch);
774 self.advance();
775 } else {
776 break;
777 }
778 }
779
780 let kind = match ident.as_str() {
781 "pipeline" => TokenKind::Pipeline,
782 "extends" => TokenKind::Extends,
783 "override" => TokenKind::Override,
784 "let" => TokenKind::Let,
785 "const" => TokenKind::Const,
786 "var" => TokenKind::Var,
787 "if" => TokenKind::If,
788 "else" => TokenKind::Else,
789 "for" => TokenKind::For,
790 "in" => TokenKind::In,
791 "match" => TokenKind::Match,
792 "retry" => TokenKind::Retry,
793 "parallel" => TokenKind::Parallel,
794 "return" => TokenKind::Return,
795 "import" => TokenKind::Import,
796 "true" => TokenKind::True,
797 "false" => TokenKind::False,
798 "nil" => TokenKind::Nil,
799 "try" => TokenKind::Try,
800 "catch" => TokenKind::Catch,
801 "throw" => TokenKind::Throw,
802 "finally" => TokenKind::Finally,
803 "fn" => TokenKind::Fn,
804 "spawn" => TokenKind::Spawn,
805 "while" => TokenKind::While,
806 "type" => TokenKind::TypeKw,
807 "enum" => TokenKind::Enum,
808 "eval_pack" => TokenKind::EvalPack,
809 "struct" => TokenKind::Struct,
810 "interface" => TokenKind::Interface,
811 "emit" => TokenKind::Emit,
812 "pub" => TokenKind::Pub,
813 "from" => TokenKind::From,
814 "to" => TokenKind::To,
815 "tool" => TokenKind::Tool,
816 "exclusive" => TokenKind::Exclusive,
817 "guard" => TokenKind::Guard,
818 "require" => TokenKind::Require,
819 "deadline" => TokenKind::Deadline,
820 "defer" => TokenKind::Defer,
821 "yield" => TokenKind::Yield,
822 "mutex" => TokenKind::Mutex,
823 "break" => TokenKind::Break,
824 "continue" => TokenKind::Continue,
825 "select" => TokenKind::Select,
826 "impl" => TokenKind::Impl,
827 "skill" => TokenKind::Skill,
828 "request_approval" => TokenKind::RequestApproval,
829 "dual_control" => TokenKind::DualControl,
830 "ask_user" => TokenKind::AskUser,
831 "escalate_to" => TokenKind::EscalateTo,
832 _ => TokenKind::Identifier(ident),
833 };
834
835 Token::with_span(
836 kind,
837 Span::with_offsets(start_byte, self.byte_pos, self.line, start_col),
838 )
839 }
840
841 fn try_two_char_op(&mut self) -> Option<Token> {
842 if self.pos >= self.source.len() {
843 return None;
844 }
845 let ch = self.source[self.pos];
846 let next = self.peek()?;
847
848 let kind = match (ch, next) {
849 ('=', '=') => TokenKind::Eq,
850 ('!', '=') => TokenKind::Neq,
851 ('&', '&') => TokenKind::And,
852 ('|', '|') => TokenKind::Or,
853 ('|', '>') => TokenKind::Pipe,
854 ('?', '?') => TokenKind::NilCoal,
855 ('*', '*') => TokenKind::Pow,
856 ('?', '.') => TokenKind::QuestionDot,
857 ('-', '>') => TokenKind::Arrow,
858 ('-', '=') => TokenKind::MinusAssign,
859 ('+', '=') => TokenKind::PlusAssign,
860 ('*', '=') => TokenKind::StarAssign,
861 ('/', '=') => TokenKind::SlashAssign,
862 ('%', '=') => TokenKind::PercentAssign,
863 ('<', '=') => TokenKind::Lte,
864 ('>', '=') => TokenKind::Gte,
865 _ => return None,
866 };
867
868 let start_byte = self.byte_pos;
869 let col = self.column;
870 self.advance();
871 self.advance();
872 Some(Token::with_span(
873 kind,
874 Span::with_offsets(start_byte, self.byte_pos, self.line, col),
875 ))
876 }
877
878 fn single_char_token(&self, ch: char) -> Option<TokenKind> {
879 match ch {
880 '{' => Some(TokenKind::LBrace),
881 '}' => Some(TokenKind::RBrace),
882 '(' => Some(TokenKind::LParen),
883 ')' => Some(TokenKind::RParen),
884 '[' => Some(TokenKind::LBracket),
885 ']' => Some(TokenKind::RBracket),
886 ',' => Some(TokenKind::Comma),
887 ':' => Some(TokenKind::Colon),
888 ';' => Some(TokenKind::Semicolon),
889 '.' => Some(TokenKind::Dot),
890 '=' => Some(TokenKind::Assign),
891 '!' => Some(TokenKind::Not),
892 '+' => Some(TokenKind::Plus),
893 '-' => Some(TokenKind::Minus),
894 '*' => Some(TokenKind::Star),
895 '/' => Some(TokenKind::Slash),
896 '%' => Some(TokenKind::Percent),
897 '<' => Some(TokenKind::Lt),
898 '>' => Some(TokenKind::Gt),
899 '?' => Some(TokenKind::Question),
900 '|' => Some(TokenKind::Bar),
901 '&' => Some(TokenKind::Amp),
902 '@' => Some(TokenKind::At),
903 _ => None,
904 }
905 }
906}
907
908fn is_duration_suffix_boundary(ch: Option<&char>) -> bool {
909 ch.is_none_or(|c| !c.is_alphanumeric() && *c != '_')
910}
911
912fn strip_common_indent(text: &str) -> String {
914 let lines: Vec<&str> = text.split('\n').collect();
915 let content_lines: Vec<&&str> = lines.iter().filter(|l| !l.trim().is_empty()).collect();
916
917 if content_lines.is_empty() {
918 return text.to_string();
919 }
920
921 let min_indent = content_lines
922 .iter()
923 .map(|line| line.chars().take_while(|c| *c == ' ' || *c == '\t').count())
924 .min()
925 .unwrap_or(0);
926
927 if min_indent == 0 {
928 return text.strip_suffix('\n').unwrap_or(text).to_string();
929 }
930
931 let stripped: String = lines
932 .iter()
933 .map(|line| {
934 if line.trim().is_empty() {
935 ""
936 } else {
937 let skip = min_indent.min(line.len());
938 &line[skip..]
939 }
940 })
941 .collect::<Vec<&str>>()
942 .join("\n");
943
944 stripped.strip_suffix('\n').unwrap_or(&stripped).to_string()
945}
946
947fn common_indent(text: &str) -> usize {
949 text.split('\n')
950 .filter(|l| !l.trim().is_empty())
951 .map(|line| line.chars().take_while(|c| *c == ' ' || *c == '\t').count())
952 .min()
953 .unwrap_or(0)
954}
955
956fn strip_indent(text: &str, n: usize) -> String {
958 let lines: Vec<&str> = text.split('\n').collect();
959 let stripped: String = lines
960 .iter()
961 .map(|line| {
962 if line.trim().is_empty() {
963 ""
964 } else {
965 let ws = line.chars().take_while(|c| *c == ' ' || *c == '\t').count();
966 let skip = n.min(ws);
967 &line[skip..]
968 }
969 })
970 .collect::<Vec<&str>>()
971 .join("\n");
972 stripped.strip_suffix('\n').unwrap_or(&stripped).to_string()
973}
974
975pub fn escape_string_literal(value: &str) -> String {
984 let mut out = String::with_capacity(value.len());
985 let mut chars = value.chars().peekable();
986 while let Some(ch) = chars.next() {
987 match ch {
988 '\\' => out.push_str("\\\\"),
989 '"' => out.push_str("\\\""),
990 '\n' => out.push_str("\\n"),
991 '\r' => out.push_str("\\r"),
992 '\t' => out.push_str("\\t"),
993 '\0' => out.push_str("\\0"),
994 '$' if chars.peek() == Some(&'{') => out.push_str("\\$"),
995 _ => out.push(ch),
996 }
997 }
998 out
999}
1000
1001fn strip_trailing_newline_segments(mut segments: Vec<StringSegment>) -> Vec<StringSegment> {
1004 if let Some(StringSegment::Literal(s)) = segments.last() {
1005 if s.trim().is_empty() {
1006 segments.pop();
1007 }
1008 }
1009 segments
1010}
1011
1012#[cfg(test)]
1013mod tests {
1014 use super::*;
1015
1016 #[test]
1017 fn escape_string_literal_round_trips_through_the_lexer() {
1018 for original in [
1019 "plain text",
1020 "with \"quotes\" and \\backslash",
1021 "newline\ntab\tcr\r",
1022 "interpolation ${1 + 1} stays text",
1023 "already-escaped \\${x}",
1024 "plain $dollar and ${nested ${inner}}",
1025 ] {
1026 let literal = format!("\"{}\"", escape_string_literal(original));
1027 let mut lexer = Lexer::new(&literal);
1028 let tokens = lexer.tokenize().expect("escaped literal must lex");
1029 let TokenKind::StringLiteral(ref value) = tokens[0].kind else {
1030 panic!(
1031 "expected a plain string token for {literal:?}, got {:?}",
1032 tokens[0].kind
1033 );
1034 };
1035 assert_eq!(value, original, "round trip through {literal:?}");
1036 }
1037 }
1038
1039 #[test]
1040 fn shebang_at_offset_zero_is_skipped() {
1041 let src = "#!/usr/bin/env harn\nlet x = 1";
1042 let mut lexer = Lexer::new(src);
1043 let tokens = lexer.tokenize().expect("shebang should be skipped");
1044 assert_eq!(tokens[0].kind, TokenKind::Newline);
1046 assert_eq!(tokens[1].kind, TokenKind::Let);
1047 assert!(matches!(&tokens[2].kind, TokenKind::Identifier(n) if n == "x"));
1048 }
1049
1050 #[test]
1051 fn shebang_without_trailing_newline_is_skipped() {
1052 let src = "#!/usr/bin/env harn";
1053 let mut lexer = Lexer::new(src);
1054 let tokens = lexer.tokenize().expect("shebang at EOF should be skipped");
1055 let non_eof: Vec<_> = tokens
1057 .iter()
1058 .filter(|t| !matches!(t.kind, TokenKind::Eof))
1059 .collect();
1060 assert!(
1061 non_eof.is_empty(),
1062 "expected only EOF after shebang-only file, got {non_eof:?}"
1063 );
1064 }
1065
1066 #[test]
1067 fn hash_in_middle_of_file_still_errors() {
1068 let src = "let x = 1\n# not a shebang\n";
1069 let mut lexer = Lexer::new(src);
1070 let result = lexer.tokenize();
1071 assert!(
1072 matches!(result, Err(LexerError::UnexpectedCharacter('#', _))),
1073 "got {result:?}"
1074 );
1075 }
1076
1077 #[test]
1078 fn test_keywords() {
1079 let mut lexer = Lexer::new("pipeline let var if else for in require");
1080 let tokens = lexer.tokenize().unwrap();
1081 assert_eq!(tokens[0].kind, TokenKind::Pipeline);
1082 assert_eq!(tokens[1].kind, TokenKind::Let);
1083 assert_eq!(tokens[2].kind, TokenKind::Var);
1084 assert_eq!(tokens[3].kind, TokenKind::If);
1085 assert_eq!(tokens[4].kind, TokenKind::Else);
1086 assert_eq!(tokens[5].kind, TokenKind::For);
1087 assert_eq!(tokens[6].kind, TokenKind::In);
1088 assert_eq!(tokens[7].kind, TokenKind::Require);
1089 }
1090
1091 #[test]
1092 fn test_keywords_const_covers_lexer() {
1093 for kw in KEYWORDS {
1097 let mut lexer = Lexer::new(kw);
1098 let tokens = lexer.tokenize().expect("lex keyword");
1099 let first = &tokens[0].kind;
1100 assert!(
1101 !matches!(first, TokenKind::Identifier(_)),
1102 "keyword `{kw}` lexes as Identifier — KEYWORDS const and lexer match are out of sync"
1103 );
1104 }
1105 }
1106
1107 #[test]
1108 fn test_parallel_keyword() {
1109 let mut lexer = Lexer::new("parallel defer");
1110 let tokens = lexer.tokenize().unwrap();
1111 assert_eq!(tokens[0].kind, TokenKind::Parallel);
1112 assert_eq!(tokens[1].kind, TokenKind::Defer);
1113 }
1114
1115 #[test]
1116 fn test_numbers() {
1117 let mut lexer = Lexer::new("42 3.14");
1118 let tokens = lexer.tokenize().unwrap();
1119 assert_eq!(tokens[0].kind, TokenKind::IntLiteral(42));
1120 #[allow(clippy::approx_constant)]
1121 let expected = 3.14;
1122 assert_eq!(tokens[1].kind, TokenKind::FloatLiteral(expected));
1123 }
1124
1125 #[test]
1126 fn test_int_literal_max_is_exact_and_overflow_is_an_error() {
1127 let mut lexer = Lexer::new("9223372036854775807");
1129 let tokens = lexer.tokenize().unwrap();
1130 assert_eq!(tokens[0].kind, TokenKind::IntLiteral(i64::MAX));
1131
1132 for src in ["9223372036854775808", "99999999999999999999999"] {
1136 let mut lexer = Lexer::new(src);
1137 assert!(
1138 matches!(
1139 lexer.tokenize(),
1140 Err(LexerError::IntegerLiteralOutOfRange(lit, _)) if lit == src
1141 ),
1142 "expected out-of-range error for {src}"
1143 );
1144 }
1145
1146 let mut lexer = Lexer::new("9223372036854775808.0");
1148 let tokens = lexer.tokenize().unwrap();
1149 assert!(matches!(tokens[0].kind, TokenKind::FloatLiteral(_)));
1150 }
1151
1152 #[test]
1153 fn test_duration_suffix_requires_identifier_boundary() {
1154 let mut lexer = Lexer::new("1ms 1msfoo 2h_task 3s.ok");
1155 let tokens = lexer.tokenize().unwrap();
1156 assert_eq!(tokens[0].kind, TokenKind::DurationLiteral(1));
1157 assert_eq!(tokens[1].kind, TokenKind::IntLiteral(1));
1158 assert!(matches!(&tokens[2].kind, TokenKind::Identifier(name) if name == "msfoo"));
1159 assert_eq!(tokens[3].kind, TokenKind::IntLiteral(2));
1160 assert!(matches!(&tokens[4].kind, TokenKind::Identifier(name) if name == "h_task"));
1161 assert_eq!(tokens[5].kind, TokenKind::DurationLiteral(3000));
1162 assert_eq!(tokens[6].kind, TokenKind::Dot);
1163 }
1164
1165 #[test]
1166 fn test_duration_suffix_overflow_saturates() {
1167 let mut lexer = Lexer::new("18446744073709551615w");
1168 let tokens = lexer.tokenize().unwrap();
1169 assert_eq!(tokens[0].kind, TokenKind::DurationLiteral(u64::MAX));
1170 }
1171
1172 #[test]
1173 fn test_string() {
1174 let mut lexer = Lexer::new(r#""hello world""#);
1175 let tokens = lexer.tokenize().unwrap();
1176 assert_eq!(
1177 tokens[0].kind,
1178 TokenKind::StringLiteral("hello world".into())
1179 );
1180 }
1181
1182 #[test]
1183 fn test_interpolated_string() {
1184 let mut lexer = Lexer::new(r#""hello ${name}!""#);
1185 let tokens = lexer.tokenize().unwrap();
1186 if let TokenKind::InterpolatedString(segs) = &tokens[0].kind {
1187 assert_eq!(segs.len(), 3);
1188 assert_eq!(segs[0], StringSegment::Literal("hello ".into()));
1189 assert!(matches!(&segs[1], StringSegment::Expression(e, _, _) if e == "name"));
1190 assert_eq!(segs[2], StringSegment::Literal("!".into()));
1191 } else {
1192 panic!("Expected interpolated string");
1193 }
1194 }
1195
1196 fn single_interpolation_expr(src: &str) -> String {
1198 let mut lexer = Lexer::new(src);
1199 let tokens = lexer.tokenize().unwrap();
1200 match &tokens[0].kind {
1201 TokenKind::InterpolatedString(segs) => segs
1202 .iter()
1203 .find_map(|s| match s {
1204 StringSegment::Expression(e, _, _) => Some(e.clone()),
1205 _ => None,
1206 })
1207 .expect("expected an interpolation expression segment"),
1208 other => panic!("expected interpolated string, got {other:?}"),
1209 }
1210 }
1211
1212 #[test]
1213 fn test_interpolation_capture_is_string_literal_aware() {
1214 assert_eq!(
1216 single_interpolation_expr(r#""${x ?? "a}b"}""#),
1217 r#"x ?? "a}b""#
1218 );
1219 assert_eq!(single_interpolation_expr(r#""${f("}")}""#), r#"f("}")"#);
1220 assert_eq!(
1221 single_interpolation_expr(r#""${items["a}b"]}""#),
1222 r#"items["a}b"]"#
1223 );
1224 assert_eq!(
1227 single_interpolation_expr(r#""${x ?? "a\"b"}""#),
1228 r#"x ?? "a\"b""#
1229 );
1230 }
1231
1232 #[test]
1233 fn test_interpolation_escaped_outer_quote_is_rejected() {
1234 let mut lexer = Lexer::new(r#""${x ?? \"y\"}""#);
1239 assert!(matches!(
1240 lexer.tokenize(),
1241 Err(LexerError::UnexpectedCharacter('\\', _))
1242 ));
1243 }
1244
1245 #[test]
1246 fn test_empty_interpolation_rejected_in_single_and_multiline_strings() {
1247 let mut single = Lexer::new(r#""hello ${}""#);
1248 assert!(matches!(
1249 single.tokenize(),
1250 Err(LexerError::UnexpectedCharacter('}', _))
1251 ));
1252
1253 let mut multiline = Lexer::new("\"\"\"\nhello ${}\n\"\"\"");
1254 assert!(matches!(
1255 multiline.tokenize(),
1256 Err(LexerError::UnexpectedCharacter('}', _))
1257 ));
1258 }
1259
1260 #[test]
1261 fn test_multiline_string_escaped_dollar_before_interpolation() {
1262 let mut lexer = Lexer::new("\"\"\"\n hi \\${VAR}\n hello ${name}\n\"\"\"");
1263 let tokens = lexer.tokenize().unwrap();
1264 if let TokenKind::InterpolatedString(segs) = &tokens[0].kind {
1265 assert_eq!(segs.len(), 2);
1266 assert_eq!(segs[0], StringSegment::Literal("hi ${VAR}\nhello ".into()));
1267 assert!(matches!(&segs[1], StringSegment::Expression(e, _, _) if e == "name"));
1268 } else {
1269 panic!("Expected interpolated string");
1270 }
1271 }
1272
1273 #[test]
1274 fn test_multiline_string_escaped_dollar_without_interpolation() {
1275 let mut lexer = Lexer::new("\"\"\"\n hi \\${VAR}\n\"\"\"");
1276 let tokens = lexer.tokenize().unwrap();
1277 assert_eq!(tokens[0].kind, TokenKind::StringLiteral("hi ${VAR}".into()));
1278 }
1279
1280 #[test]
1281 fn test_multiline_string_preserves_non_interpolation_dollar_escape() {
1282 let mut lexer = Lexer::new("\"\"\"\n echo \\$PATH\n\"\"\"");
1283 let tokens = lexer.tokenize().unwrap();
1284 assert_eq!(
1285 tokens[0].kind,
1286 TokenKind::StringLiteral("echo \\$PATH".into())
1287 );
1288 }
1289
1290 #[test]
1291 fn test_interpolated_string_multiline_expression_tracks_lines() {
1292 let src = "let x = \"${render(\n \"a\",\n b,\n)}\"\nlet y = 1\n";
1299 let mut lexer = Lexer::new(src);
1300 let tokens = lexer.tokenize().unwrap();
1301 let let_y = tokens
1303 .iter()
1304 .skip(1) .find(|t| matches!(t.kind, TokenKind::Let))
1306 .expect("second `let`");
1307 assert_eq!(let_y.span.line, 5);
1308 }
1309
1310 #[test]
1311 fn test_two_char_operators() {
1312 let mut lexer = Lexer::new("== != && || |> ?? ** -> <= >=");
1313 let tokens = lexer.tokenize().unwrap();
1314 assert_eq!(tokens[0].kind, TokenKind::Eq);
1315 assert_eq!(tokens[1].kind, TokenKind::Neq);
1316 assert_eq!(tokens[2].kind, TokenKind::And);
1317 assert_eq!(tokens[3].kind, TokenKind::Or);
1318 assert_eq!(tokens[4].kind, TokenKind::Pipe);
1319 assert_eq!(tokens[5].kind, TokenKind::NilCoal);
1320 assert_eq!(tokens[6].kind, TokenKind::Pow);
1321 assert_eq!(tokens[7].kind, TokenKind::Arrow);
1322 assert_eq!(tokens[8].kind, TokenKind::Lte);
1323 assert_eq!(tokens[9].kind, TokenKind::Gte);
1324 }
1325
1326 #[test]
1327 fn test_block_comments() {
1328 let mut lexer = Lexer::new("/* outer /* nested */ still */ 42");
1329 let tokens = lexer.tokenize().unwrap();
1330 assert_eq!(tokens[0].kind, TokenKind::IntLiteral(42));
1331 }
1332
1333 #[test]
1334 fn test_multiline_block_comment_span_starts_at_open() {
1335 let src = "let a = 1\n/* block\n spanning\n lines */\nlet b = 2";
1341 let mut lex = Lexer::new(src);
1342 let tokens = lex.tokenize_with_comments().unwrap();
1343 let block = tokens
1344 .iter()
1345 .find(|t| matches!(t.kind, TokenKind::BlockComment { .. }))
1346 .expect("block comment token");
1347 assert_eq!(block.span.line, 2, "start line should be the opening `/*`");
1348 assert_eq!(
1349 block.span.column, 1,
1350 "start column should be the `/*` column"
1351 );
1352 assert_eq!(
1353 block.span.end_line, 4,
1354 "end line should be the closing `*/`"
1355 );
1356 }
1357
1358 #[test]
1359 fn test_line_comment() {
1360 let mut lexer = Lexer::new("42 // comment\n43");
1361 let tokens = lexer.tokenize().unwrap();
1362 assert_eq!(tokens[0].kind, TokenKind::IntLiteral(42));
1363 assert_eq!(tokens[1].kind, TokenKind::Newline);
1364 assert_eq!(tokens[2].kind, TokenKind::IntLiteral(43));
1365 }
1366
1367 #[test]
1368 fn test_doc_line_comment_detection() {
1369 let cases = [
1370 ("// regular", false),
1371 ("/// doc", true),
1372 ("//// separator bar", false),
1373 ("///// also a bar", false),
1374 ("///", true), ];
1376 for (src, expect_doc) in cases {
1377 let mut lex = Lexer::new(src);
1378 let tokens = lex.tokenize_with_comments().unwrap();
1379 match &tokens[0].kind {
1380 TokenKind::LineComment { is_doc, .. } => {
1381 assert_eq!(
1382 *is_doc, expect_doc,
1383 "expected is_doc={expect_doc} for input {src:?}",
1384 );
1385 }
1386 other => panic!("expected LineComment for {src:?}, got {other:?}"),
1387 }
1388 }
1389 }
1390
1391 #[test]
1392 fn test_doc_block_comment_detection() {
1393 let cases = [
1394 ("/* regular */", false),
1395 ("/** doc */", true),
1396 ("/*** not a doc */", false),
1397 ("/**/", false), ];
1399 for (src, expect_doc) in cases {
1400 let mut lex = Lexer::new(src);
1401 let tokens = lex.tokenize_with_comments().unwrap();
1402 match &tokens[0].kind {
1403 TokenKind::BlockComment { is_doc, .. } => {
1404 assert_eq!(
1405 *is_doc, expect_doc,
1406 "expected is_doc={expect_doc} for input {src:?}",
1407 );
1408 }
1409 other => panic!("expected BlockComment for {src:?}, got {other:?}"),
1410 }
1411 }
1412 }
1413
1414 #[test]
1415 fn test_newlines() {
1416 let mut lexer = Lexer::new("a\nb");
1417 let tokens = lexer.tokenize().unwrap();
1418 assert_eq!(tokens[0].kind, TokenKind::Identifier("a".into()));
1419 assert_eq!(tokens[1].kind, TokenKind::Newline);
1420 assert_eq!(tokens[2].kind, TokenKind::Identifier("b".into()));
1421 }
1422
1423 #[test]
1424 fn test_backslash_continuation() {
1425 let mut lexer = Lexer::new("10 \\\n- 3");
1426 let tokens = lexer.tokenize().unwrap();
1427 assert_eq!(tokens[0].kind, TokenKind::IntLiteral(10));
1428 assert_eq!(tokens[1].kind, TokenKind::Minus);
1429 assert_eq!(tokens[2].kind, TokenKind::IntLiteral(3));
1430 assert_eq!(tokens.len(), 4);
1432 }
1433
1434 #[test]
1435 fn test_unexpected_character() {
1436 let mut lexer = Lexer::new("`");
1437 let err = lexer.tokenize().unwrap_err();
1438 assert!(matches!(err, LexerError::UnexpectedCharacter('`', _)));
1439 }
1440
1441 #[test]
1442 fn test_at_token() {
1443 let mut lexer = Lexer::new("@deprecated");
1444 let tokens = lexer.tokenize().unwrap();
1445 assert_eq!(tokens[0].kind, TokenKind::At);
1446 assert_eq!(tokens[1].kind, TokenKind::Identifier("deprecated".into()));
1447 }
1448
1449 #[test]
1450 fn test_unterminated_string() {
1451 let mut lexer = Lexer::new("\"unterminated");
1452 let err = lexer.tokenize().unwrap_err();
1453 assert!(matches!(err, LexerError::UnterminatedString(_)));
1454 }
1455
1456 #[test]
1457 fn test_escape_sequences() {
1458 let mut lexer = Lexer::new(r#""a\nb\t\\""#);
1459 let tokens = lexer.tokenize().unwrap();
1460 assert_eq!(tokens[0].kind, TokenKind::StringLiteral("a\nb\t\\".into()));
1461 }
1462
1463 #[test]
1464 fn test_escape_carriage_return_and_null() {
1465 let mut lexer = Lexer::new(r#""a\rb\0c""#);
1466 let tokens = lexer.tokenize().unwrap();
1467 assert_eq!(tokens[0].kind, TokenKind::StringLiteral("a\rb\0c".into()));
1468 }
1469
1470 #[test]
1471 fn test_number_then_dot_method() {
1472 let mut lexer = Lexer::new("42.method");
1473 let tokens = lexer.tokenize().unwrap();
1474 assert_eq!(tokens[0].kind, TokenKind::IntLiteral(42));
1475 assert_eq!(tokens[1].kind, TokenKind::Dot);
1476 assert_eq!(tokens[2].kind, TokenKind::Identifier("method".into()));
1477 }
1478
1479 #[test]
1480 fn test_hashed_raw_string_basic() {
1481 let mut lexer = Lexer::new("r#\"abc\"#");
1482 let tokens = lexer.tokenize().unwrap();
1483 assert_eq!(tokens[0].kind, TokenKind::RawStringLiteral("abc".into()));
1484 }
1485
1486 #[test]
1487 fn test_hashed_raw_string_embedded_quote() {
1488 let mut lexer = Lexer::new("r#\"a\"b\"#");
1490 let tokens = lexer.tokenize().unwrap();
1491 assert_eq!(tokens[0].kind, TokenKind::RawStringLiteral("a\"b".into()));
1492 }
1493
1494 #[test]
1495 fn test_hashed_raw_string_regex_with_quotes() {
1496 let mut lexer = Lexer::new("r#\"\"([^\"\\]*)\"\"#");
1498 let tokens = lexer.tokenize().unwrap();
1499 assert_eq!(
1500 tokens[0].kind,
1501 TokenKind::RawStringLiteral("\"([^\"\\]*)\"".into())
1502 );
1503 }
1504
1505 #[test]
1506 fn test_double_hashed_raw_string_holds_quote_hash() {
1507 let mut lexer = Lexer::new("r##\"a\"#b\"##");
1510 let tokens = lexer.tokenize().unwrap();
1511 assert_eq!(tokens[0].kind, TokenKind::RawStringLiteral("a\"#b".into()));
1512 }
1513
1514 #[test]
1515 fn test_plain_raw_string_still_works() {
1516 let mut lexer = Lexer::new("r\"plain\"");
1517 let tokens = lexer.tokenize().unwrap();
1518 assert_eq!(tokens[0].kind, TokenKind::RawStringLiteral("plain".into()));
1519 }
1520
1521 #[test]
1522 fn test_hashed_raw_string_unterminated_errors() {
1523 let mut lexer = Lexer::new("r#\"no close\"");
1524 assert!(lexer.tokenize().is_err());
1525 }
1526
1527 #[test]
1528 fn test_hashed_raw_string_newline_errors() {
1529 let mut lexer = Lexer::new("r#\"line1\nline2\"#");
1530 assert!(lexer.tokenize().is_err());
1531 }
1532}