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