1use std::cmp::Ordering;
14
15use crate::error::SyntaxError;
16use crate::kind::SyntaxKind;
17use crate::text::TextRange;
18
19const TAB_WIDTH: u32 = 4;
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub struct Token {
29 pub kind: SyntaxKind,
30 pub range: TextRange,
31}
32
33impl Token {
34 #[must_use]
35 pub fn text(self, source: &str) -> &str {
36 self.range.slice(source)
37 }
38}
39
40#[derive(Debug, Clone)]
42pub struct LexResult {
43 pub tokens: Vec<Token>,
44 pub errors: Vec<SyntaxError>,
45}
46
47#[derive(Debug, Clone, Copy)]
63struct LambdaContext {
64 bracket_depth: u32,
67 body_column: Option<u32>,
75 indent_len: usize,
78 inline: bool,
92}
93
94#[must_use]
99pub fn tokenize(source: &str) -> LexResult {
100 Lexer::new(source).run()
101}
102
103struct Lexer<'a> {
104 source: &'a str,
105 bytes: &'a [u8],
106 pos: u32,
107 tokens: Vec<Token>,
108 errors: Vec<SyntaxError>,
109 indents: Vec<u32>,
111 bracket_depth: u32,
114 lambda_stack: Vec<LambdaContext>,
116 pending_lambda: Option<u32>,
119 at_line_start: bool,
121}
122
123impl<'a> Lexer<'a> {
124 fn new(source: &'a str) -> Self {
125 Self {
126 source,
127 bytes: source.as_bytes(),
128 pos: 0,
129 tokens: Vec::with_capacity(source.len() / 4),
131 errors: Vec::new(),
132 indents: vec![0],
133 bracket_depth: 0,
134 lambda_stack: Vec::new(),
135 pending_lambda: None,
136 at_line_start: true,
137 }
138 }
139
140 fn run(mut self) -> LexResult {
141 if self.source.starts_with('\u{feff}') {
151 let start = self.pos;
152 self.pos += '\u{feff}'.len_utf8() as u32;
153 self.push(SyntaxKind::Whitespace, TextRange::new(start, self.pos));
154 }
155
156 while !self.at_eof() {
157 if self.at_line_start && self.indent_significant() {
158 self.lex_line_start();
159 } else {
160 self.lex_token();
161 }
162 }
163
164 let end = self.pos;
166 while !self.lambda_stack.is_empty() {
167 self.close_top_lambda(end);
168 }
169 while self.indents.len() > 1 {
170 self.indents.pop();
171 self.push(SyntaxKind::Dedent, TextRange::empty(end));
172 }
173 self.push(SyntaxKind::Eof, TextRange::empty(end));
174
175 LexResult {
176 tokens: self.tokens,
177 errors: self.errors,
178 }
179 }
180
181 fn lex_line_start(&mut self) {
189 let start = self.pos;
190 let mut column = 0;
191 let mut cursor = self.pos;
192 while let Some(byte) = self.byte_at(cursor) {
193 match byte {
194 b' ' => column += 1,
195 b'\t' => column = (column / TAB_WIDTH + 1) * TAB_WIDTH,
197 _ => break,
198 }
199 cursor += 1;
200 }
201
202 let indent_range = TextRange::new(start, cursor);
203 let blank_line = matches!(
206 self.byte_at(cursor),
207 None | Some(b'\n' | b'\r' | b'#' | b'\\')
208 );
209
210 if blank_line {
211 if !indent_range.is_empty() {
214 self.pos = cursor;
215 self.push(SyntaxKind::Whitespace, indent_range);
216 }
217 self.at_line_start = false;
218 self.lex_token();
220 return;
221 }
222
223 while let Some(context) = self.lambda_stack.last() {
227 match context.body_column {
228 None if context.inline => self.close_top_lambda(start),
230 None => {
231 self.lambda_stack
232 .last_mut()
233 .expect("checked immediately above")
234 .body_column = Some(column);
235 break;
236 }
237 Some(body_column) if column < body_column => self.close_top_lambda(start),
238 Some(_) => break,
239 }
240 }
241
242 if !self.indent_significant() {
245 if !indent_range.is_empty() {
246 self.pos = cursor;
247 self.push(SyntaxKind::Whitespace, indent_range);
248 }
249 self.at_line_start = false;
250 return;
251 }
252
253 let current = *self.indents.last().expect("indent stack is never empty");
256 match column.cmp(¤t) {
257 Ordering::Greater => {
258 self.pos = cursor;
259 self.emit_indent_whitespace(indent_range);
260 self.indents.push(column);
261 self.push(SyntaxKind::Indent, TextRange::empty(cursor));
262 }
263 Ordering::Less => {
264 while *self.indents.last().expect("indent stack is never empty") > column {
265 self.indents.pop();
266 self.push(SyntaxKind::Dedent, TextRange::empty(start));
267 }
268 if *self.indents.last().expect("indent stack is never empty") != column {
269 self.errors.push(SyntaxError::new(
270 indent_range,
271 "unindent does not match any outer indentation level",
272 ));
273 self.indents.push(column);
275 }
276 self.pos = cursor;
277 self.emit_indent_whitespace(indent_range);
278 }
279 Ordering::Equal => {
280 self.pos = cursor;
281 self.emit_indent_whitespace(indent_range);
282 }
283 }
284
285 self.at_line_start = false;
286 }
287
288 fn emit_indent_whitespace(&mut self, indent_range: TextRange) {
290 if !indent_range.is_empty() {
291 self.push(SyntaxKind::Whitespace, indent_range);
292 }
293 }
294
295 fn lex_token(&mut self) {
298 let start = self.pos;
299 let Some(byte) = self.byte_at(self.pos) else {
300 return;
301 };
302
303 let kind = match byte {
304 b' ' | b'\t' => {
305 self.bump_while(|b| b == b' ' || b == b'\t');
306 SyntaxKind::Whitespace
307 }
308 b'\n' => {
309 self.pos += 1;
310 self.finish_line();
311 SyntaxKind::Newline
312 }
313 b'\r' => {
314 self.pos += 1;
315 if self.byte_at(self.pos) == Some(b'\n') {
316 self.pos += 1;
317 }
318 self.finish_line();
319 SyntaxKind::Newline
320 }
321 b'\\' => self.lex_backslash(),
322 b'#' => self.lex_comment(),
323 b'0'..=b'9' => self.lex_number(),
324 b'.' if matches!(self.byte_at(self.pos + 1), Some(b'0'..=b'9')) => self.lex_number(),
325 b'"' | b'\'' => self.lex_string(),
326 b'$' => self.lex_node_path(SyntaxKind::GetNode),
327 b'&' | b'^' if self.starts_annotated_string() => self.lex_annotated_string(byte),
328 b'%' if self.starts_unique_node() => self.lex_node_path(SyntaxKind::UniqueNode),
329 b if is_ident_start(b) => self.lex_ident_or_keyword(),
330 _ => self.lex_operator(),
331 };
332
333 if self.pos == start && kind == SyntaxKind::Unknown {
335 self.pos += 1;
336 }
337
338 if kind == SyntaxKind::FuncKw && self.bracket_depth > 0 {
341 self.pending_lambda = Some(self.bracket_depth);
342 } else if kind == SyntaxKind::Colon
343 && self.bracket_depth > 0
344 && self.pending_lambda == Some(self.bracket_depth)
345 {
346 self.pending_lambda = None;
347 self.lambda_stack.push(LambdaContext {
348 bracket_depth: self.bracket_depth,
349 body_column: None,
350 indent_len: self.indents.len(),
351 inline: false,
352 });
353 } else if kind != SyntaxKind::Newline
354 && let Some(context) = self.lambda_stack.last_mut()
355 && context.body_column.is_none()
356 {
357 context.inline = true;
360 }
361
362 if kind != SyntaxKind::Newline {
363 self.at_line_start = false;
364 }
365
366 self.push(kind, TextRange::new(start, self.pos));
367 }
368
369 fn indent_significant(&self) -> bool {
374 match self.lambda_stack.last() {
375 Some(context) => self.bracket_depth == context.bracket_depth,
376 None => self.bracket_depth == 0,
377 }
378 }
379
380 fn close_top_lambda(&mut self, offset: u32) {
382 let Some(context) = self.lambda_stack.pop() else {
383 return;
384 };
385 while self.indents.len() > context.indent_len {
386 self.indents.pop();
387 self.push(SyntaxKind::Dedent, TextRange::empty(offset));
388 }
389 }
390
391 fn finish_line(&mut self) {
393 self.at_line_start = true;
394 }
395
396 fn lex_backslash(&mut self) -> SyntaxKind {
397 let start = self.pos;
398 let mut cursor = self.pos + 1;
399 while matches!(self.byte_at(cursor), Some(b' ' | b'\t')) {
402 cursor += 1;
403 }
404 match self.byte_at(cursor) {
405 Some(b'\n') => {
406 self.pos = cursor + 1;
407 SyntaxKind::LineContinuation
408 }
409 Some(b'\r') => {
410 cursor += 1;
411 if self.byte_at(cursor) == Some(b'\n') {
412 cursor += 1;
413 }
414 self.pos = cursor;
415 SyntaxKind::LineContinuation
416 }
417 _ => {
418 self.pos = start + 1;
419 self.errors.push(SyntaxError::new(
420 TextRange::new(start, self.pos),
421 "stray `\\` outside a line continuation",
422 ));
423 SyntaxKind::Unknown
424 }
425 }
426 }
427
428 fn lex_comment(&mut self) -> SyntaxKind {
429 let doc = self.byte_at(self.pos + 1) == Some(b'#');
430 self.bump_while(|b| b != b'\n' && b != b'\r');
431 if doc {
432 SyntaxKind::DocComment
433 } else {
434 SyntaxKind::Comment
435 }
436 }
437
438 fn lex_number(&mut self) -> SyntaxKind {
441 let mut is_float = false;
442
443 if self.byte_at(self.pos) == Some(b'0')
444 && matches!(self.byte_at(self.pos + 1), Some(b'x' | b'X' | b'b' | b'B'))
445 {
446 let radix_marker = self.byte_at(self.pos + 1).expect("checked above");
447 self.pos += 2;
448 if radix_marker == b'x' || radix_marker == b'X' {
449 self.bump_while(|b| b.is_ascii_hexdigit() || b == b'_');
450 } else {
451 self.bump_while(|b| matches!(b, b'0' | b'1' | b'_'));
452 }
453 return SyntaxKind::Int;
454 }
455
456 self.bump_while(|b| b.is_ascii_digit() || b == b'_');
457
458 if self.byte_at(self.pos) == Some(b'.')
461 && matches!(self.byte_at(self.pos + 1), Some(b'0'..=b'9'))
462 {
463 is_float = true;
464 self.pos += 1;
465 self.bump_while(|b| b.is_ascii_digit() || b == b'_');
466 } else if self.byte_at(self.pos) == Some(b'.')
467 && !matches!(self.byte_at(self.pos + 1), Some(b'.'))
468 && !matches!(self.byte_at(self.pos + 1), Some(b) if is_ident_start(b))
469 {
470 is_float = true;
472 self.pos += 1;
473 }
474
475 if matches!(self.byte_at(self.pos), Some(b'e' | b'E')) {
476 let mut cursor = self.pos + 1;
477 if matches!(self.byte_at(cursor), Some(b'+' | b'-')) {
478 cursor += 1;
479 }
480 if matches!(self.byte_at(cursor), Some(b'0'..=b'9')) {
481 is_float = true;
482 self.pos = cursor;
483 self.bump_while(|b| b.is_ascii_digit() || b == b'_');
484 }
485 }
486
487 if is_float {
488 SyntaxKind::Float
489 } else {
490 SyntaxKind::Int
491 }
492 }
493
494 fn starts_annotated_string(&self) -> bool {
497 matches!(self.byte_at(self.pos + 1), Some(b'"' | b'\'')) && !self.prev_can_end_expr()
498 }
499
500 fn lex_annotated_string(&mut self, sigil: u8) -> SyntaxKind {
501 self.pos += 1;
502 self.lex_string();
503 if sigil == b'&' {
504 SyntaxKind::StringName
505 } else {
506 SyntaxKind::NodePath
507 }
508 }
509
510 fn starts_unique_node(&self) -> bool {
513 if self.prev_can_end_expr() {
514 return false;
515 }
516 matches!(self.byte_at(self.pos + 1), Some(b'"' | b'\''))
517 || matches!(self.byte_at(self.pos + 1), Some(b) if is_ident_start(b))
518 }
519
520 fn lex_node_path(&mut self, kind: SyntaxKind) -> SyntaxKind {
525 let sigil_start = self.pos;
526 self.pos += 1;
527
528 if matches!(self.byte_at(self.pos), Some(b'"' | b'\'')) {
529 self.lex_string();
530 return kind;
531 }
532
533 let mut matched_any = false;
534
535 if self.byte_at(self.pos) == Some(b'/') {
537 self.pos += 1;
538 matched_any = true;
539 }
540
541 loop {
542 if self.byte_at(self.pos) == Some(b'.') && self.byte_at(self.pos + 1) == Some(b'.') {
544 self.pos += 2;
545 matched_any = true;
546 } else {
547 if self.byte_at(self.pos) == Some(b'%') {
548 self.pos += 1;
549 matched_any = true;
550 }
551 if matches!(self.byte_at(self.pos), Some(b) if is_ident_start(b)) {
552 self.bump_while(is_ident_continue);
553 matched_any = true;
554 } else {
555 break;
556 }
557 }
558
559 if self.byte_at(self.pos) == Some(b'/') {
560 self.pos += 1;
561 } else {
562 break;
563 }
564 }
565
566 if !matched_any {
567 self.errors.push(SyntaxError::new(
568 TextRange::new(sigil_start, self.pos),
569 "expected a node path after the sigil",
570 ));
571 }
572 kind
573 }
574
575 fn lex_string(&mut self) -> SyntaxKind {
576 let start = self.pos;
577 let quote = self.byte_at(self.pos).expect("caller checked for a quote");
578
579 let triple =
581 self.byte_at(self.pos + 1) == Some(quote) && self.byte_at(self.pos + 2) == Some(quote);
582 let delim_len = if triple { 3 } else { 1 };
583 self.pos += delim_len;
584
585 loop {
586 let Some(byte) = self.byte_at(self.pos) else {
587 self.errors.push(SyntaxError::new(
588 TextRange::new(start, self.pos),
589 "unterminated string literal",
590 ));
591 break;
592 };
593
594 if byte == b'\\' {
597 self.pos += 1;
598 if self.pos < self.len() {
599 self.pos += 1;
600 }
601 continue;
602 }
603
604 if !triple && matches!(byte, b'\n' | b'\r') {
605 self.errors.push(SyntaxError::new(
606 TextRange::new(start, self.pos),
607 "unterminated string literal",
608 ));
609 break;
610 }
611
612 if byte == quote {
613 if triple {
614 if self.byte_at(self.pos + 1) == Some(quote)
615 && self.byte_at(self.pos + 2) == Some(quote)
616 {
617 self.pos += 3;
618 break;
619 }
620 self.pos += 1;
621 continue;
622 }
623 self.pos += 1;
624 break;
625 }
626
627 self.pos += 1;
628 }
629
630 SyntaxKind::Str
631 }
632
633 fn lex_ident_or_keyword(&mut self) -> SyntaxKind {
634 let start = self.pos;
635 self.bump_while(is_ident_continue);
636 let text = TextRange::new(start, self.pos).slice(self.source);
637
638 if text == "r" && matches!(self.byte_at(self.pos), Some(b'"' | b'\'')) {
640 self.lex_string();
641 return SyntaxKind::Str;
642 }
643
644 SyntaxKind::from_keyword(text).unwrap_or(SyntaxKind::Ident)
645 }
646
647 #[allow(clippy::too_many_lines)]
650 fn lex_operator(&mut self) -> SyntaxKind {
651 let byte = self.byte_at(self.pos).expect("caller checked for a byte");
652 let next = self.byte_at(self.pos + 1);
653 let after = self.byte_at(self.pos + 2);
654
655 match byte {
659 b')' | b']' | b'}' => {
660 while self
661 .lambda_stack
662 .last()
663 .is_some_and(|context| context.bracket_depth >= self.bracket_depth)
664 {
665 self.close_top_lambda(self.pos);
666 }
667 }
668 b',' => {
669 while self
670 .lambda_stack
671 .last()
672 .is_some_and(|context| context.bracket_depth == self.bracket_depth)
673 {
674 self.close_top_lambda(self.pos);
675 }
676 }
677 _ => {}
678 }
679
680 self.pos += 1;
681
682 macro_rules! two {
683 ($kind:expr) => {{
684 self.pos += 1;
685 $kind
686 }};
687 }
688 macro_rules! three {
689 ($kind:expr) => {{
690 self.pos += 2;
691 $kind
692 }};
693 }
694
695 match byte {
696 b'+' if next == Some(b'=') => two!(SyntaxKind::PlusEq),
697 b'+' => SyntaxKind::Plus,
698 b'-' if next == Some(b'=') => two!(SyntaxKind::MinusEq),
699 b'-' if next == Some(b'>') => two!(SyntaxKind::Arrow),
700 b'-' => SyntaxKind::Minus,
701 b'*' if next == Some(b'*') && after == Some(b'=') => three!(SyntaxKind::StarStarEq),
702 b'*' if next == Some(b'*') => two!(SyntaxKind::StarStar),
703 b'*' if next == Some(b'=') => two!(SyntaxKind::StarEq),
704 b'*' => SyntaxKind::Star,
705 b'/' if next == Some(b'=') => two!(SyntaxKind::SlashEq),
706 b'/' => SyntaxKind::Slash,
707 b'%' if next == Some(b'=') => two!(SyntaxKind::PercentEq),
708 b'%' => SyntaxKind::Percent,
709 b'=' if next == Some(b'=') => two!(SyntaxKind::EqEq),
710 b'=' => SyntaxKind::Eq,
711 b'!' if next == Some(b'=') => two!(SyntaxKind::BangEq),
712 b'!' => SyntaxKind::Bang,
713 b'<' if next == Some(b'<') && after == Some(b'=') => three!(SyntaxKind::ShlEq),
714 b'<' if next == Some(b'<') => two!(SyntaxKind::Shl),
715 b'<' if next == Some(b'=') => two!(SyntaxKind::LtEq),
716 b'<' => SyntaxKind::Lt,
717 b'>' if next == Some(b'>') && after == Some(b'=') => three!(SyntaxKind::ShrEq),
718 b'>' if next == Some(b'>') => two!(SyntaxKind::Shr),
719 b'>' if next == Some(b'=') => two!(SyntaxKind::GtEq),
720 b'>' => SyntaxKind::Gt,
721 b'&' if next == Some(b'&') => two!(SyntaxKind::AmpAmp),
722 b'&' if next == Some(b'=') => two!(SyntaxKind::AmpEq),
723 b'&' => SyntaxKind::Amp,
724 b'|' if next == Some(b'|') => two!(SyntaxKind::PipePipe),
725 b'|' if next == Some(b'=') => two!(SyntaxKind::PipeEq),
726 b'|' => SyntaxKind::Pipe,
727 b'^' if next == Some(b'=') => two!(SyntaxKind::CaretEq),
728 b'^' => SyntaxKind::Caret,
729 b'~' => SyntaxKind::Tilde,
730 b':' if next == Some(b'=') => two!(SyntaxKind::ColonEq),
731 b':' => SyntaxKind::Colon,
732 b';' => SyntaxKind::Semicolon,
733 b',' => SyntaxKind::Comma,
734 b'.' if next == Some(b'.') && after == Some(b'.') => three!(SyntaxKind::Ellipsis),
735 b'.' if next == Some(b'.') => two!(SyntaxKind::DotDot),
736 b'.' => SyntaxKind::Dot,
737 b'@' => SyntaxKind::At,
738 b'$' => SyntaxKind::Dollar,
739 b'(' => {
740 self.bracket_depth += 1;
741 SyntaxKind::LParen
742 }
743 b')' => {
744 self.bracket_depth = self.bracket_depth.saturating_sub(1);
745 SyntaxKind::RParen
746 }
747 b'[' => {
748 self.bracket_depth += 1;
749 SyntaxKind::LBracket
750 }
751 b']' => {
752 self.bracket_depth = self.bracket_depth.saturating_sub(1);
753 SyntaxKind::RBracket
754 }
755 b'{' => {
756 self.bracket_depth += 1;
757 SyntaxKind::LBrace
758 }
759 b'}' => {
760 self.bracket_depth = self.bracket_depth.saturating_sub(1);
761 SyntaxKind::RBrace
762 }
763 _ => {
764 while self.pos < self.len() && !self.source.is_char_boundary(self.pos as usize) {
767 self.pos += 1;
768 }
769 self.errors.push(SyntaxError::new(
770 TextRange::new(self.pos - 1, self.pos),
771 "unexpected character",
772 ));
773 SyntaxKind::Unknown
774 }
775 }
776 }
777
778 fn prev_can_end_expr(&self) -> bool {
791 for token in self.tokens.iter().rev() {
792 if token.kind == SyntaxKind::Newline {
795 return false;
796 }
797 if token.kind.is_trivia() {
798 continue;
799 }
800 return matches!(
801 token.kind,
802 SyntaxKind::Ident
803 | SyntaxKind::Int
804 | SyntaxKind::Float
805 | SyntaxKind::Str
806 | SyntaxKind::StringName
807 | SyntaxKind::NodePath
808 | SyntaxKind::GetNode
809 | SyntaxKind::UniqueNode
810 | SyntaxKind::RParen
811 | SyntaxKind::RBracket
812 | SyntaxKind::RBrace
813 | SyntaxKind::SelfKw
814 | SyntaxKind::SuperKw
815 | SyntaxKind::TrueKw
816 | SyntaxKind::FalseKw
817 | SyntaxKind::NullKw
818 );
819 }
820 false
821 }
822
823 fn push(&mut self, kind: SyntaxKind, range: TextRange) {
824 self.tokens.push(Token { kind, range });
825 }
826
827 fn bump_while(&mut self, predicate: impl Fn(u8) -> bool) {
828 while let Some(byte) = self.byte_at(self.pos) {
829 if !predicate(byte) {
830 break;
831 }
832 self.pos += 1;
833 }
834 }
835
836 fn byte_at(&self, pos: u32) -> Option<u8> {
837 self.bytes.get(pos as usize).copied()
838 }
839
840 fn len(&self) -> u32 {
841 self.bytes.len() as u32
842 }
843
844 fn at_eof(&self) -> bool {
845 self.pos >= self.len()
846 }
847}
848
849fn is_ident_start(byte: u8) -> bool {
850 byte.is_ascii_alphabetic() || byte == b'_' || byte >= 0x80
851}
852
853fn is_ident_continue(byte: u8) -> bool {
854 byte.is_ascii_alphanumeric() || byte == b'_' || byte >= 0x80
855}
856
857#[cfg(test)]
858mod tests {
859 use super::*;
860
861 fn assert_lossless(source: &str) {
863 let lexed = tokenize(source);
864 let rebuilt: String = lexed
865 .tokens
866 .iter()
867 .map(|token| token.text(source))
868 .collect();
869 assert_eq!(rebuilt, source, "token spans must cover the source exactly");
870 }
871
872 fn kinds(source: &str) -> Vec<SyntaxKind> {
873 tokenize(source)
874 .tokens
875 .into_iter()
876 .map(|token| token.kind)
877 .filter(|kind| !kind.is_trivia() && *kind != SyntaxKind::Eof)
878 .collect()
879 }
880
881 #[test]
882 fn round_trips_a_realistic_script() {
883 assert_lossless(
884 "@tool\nclass_name Player\nextends CharacterBody2D\n\n## Docs.\nsignal died\n\nconst MAX := 100\n\n\nfunc _ready() -> void:\n\tvar x := [1, 2, {\"a\": 1}] # trailing\n\tif x and true:\n\t\tprint($Sprite2D/Label)\n",
885 );
886 }
887
888 #[test]
889 fn round_trips_edge_cases() {
890 for source in [
891 "",
892 "\n",
893 "\n\n\n",
894 "pass",
895 "\tpass\n",
896 "a\r\nb\r\n",
897 "var s = \"unterminated\n",
898 "if a \\\n\tand b:\n\tpass\n",
899 "x = 1 § 2\n",
900 "func f():\n\t\t\tpass\n\treturn\n",
901 ] {
902 assert_lossless(source);
903 }
904 }
905
906 #[test]
907 fn emits_indent_and_dedent_around_blocks() {
908 let kinds = kinds("func f():\n\tpass\nvar x = 1\n");
909 assert_eq!(
910 kinds,
911 vec![
912 SyntaxKind::FuncKw,
913 SyntaxKind::Ident,
914 SyntaxKind::LParen,
915 SyntaxKind::RParen,
916 SyntaxKind::Colon,
917 SyntaxKind::Indent,
918 SyntaxKind::PassKw,
919 SyntaxKind::Dedent,
920 SyntaxKind::VarKw,
921 SyntaxKind::Ident,
922 SyntaxKind::Eq,
923 SyntaxKind::Int,
924 ]
925 );
926 }
927
928 #[test]
929 fn blank_and_comment_lines_do_not_shift_indentation() {
930 let kinds = kinds("func f():\n\tvar a = 1\n\n# note\n\tvar b = 2\n");
932 assert_eq!(
933 kinds.iter().filter(|k| **k == SyntaxKind::Indent).count(),
934 1
935 );
936 assert_eq!(
937 kinds.iter().filter(|k| **k == SyntaxKind::Dedent).count(),
938 1
939 );
940 }
941
942 #[test]
943 fn newlines_inside_brackets_are_not_line_breaks() {
944 let kinds = kinds("var a = [\n\t1,\n\t2,\n]\n");
946 assert!(!kinds.contains(&SyntaxKind::Indent));
947 assert!(!kinds.contains(&SyntaxKind::Dedent));
948 }
949
950 #[test]
951 fn distinguishes_modulo_from_unique_node() {
952 assert_eq!(
953 kinds("var a = b % c\n"),
954 vec![
955 SyntaxKind::VarKw,
956 SyntaxKind::Ident,
957 SyntaxKind::Eq,
958 SyntaxKind::Ident,
959 SyntaxKind::Percent,
960 SyntaxKind::Ident,
961 ]
962 );
963 assert_eq!(
964 kinds("var a = %HealthBar\n"),
965 vec![
966 SyntaxKind::VarKw,
967 SyntaxKind::Ident,
968 SyntaxKind::Eq,
969 SyntaxKind::UniqueNode,
970 ]
971 );
972 assert_eq!(
973 kinds("print(%Bar, a % 2)\n"),
974 vec![
975 SyntaxKind::Ident,
976 SyntaxKind::LParen,
977 SyntaxKind::UniqueNode,
978 SyntaxKind::Comma,
979 SyntaxKind::Ident,
980 SyntaxKind::Percent,
981 SyntaxKind::Int,
982 SyntaxKind::RParen,
983 ]
984 );
985 }
986
987 #[test]
988 fn lexes_node_paths_as_single_tokens() {
989 assert_eq!(kinds("$Sprite2D\n"), vec![SyntaxKind::GetNode]);
990 assert_eq!(kinds("$../Sibling/%Unique\n"), vec![SyntaxKind::GetNode]);
991 assert_eq!(kinds("$\"quoted/path\"\n"), vec![SyntaxKind::GetNode]);
992 assert_eq!(
994 kinds("$Sprite2D.position\n"),
995 vec![SyntaxKind::GetNode, SyntaxKind::Dot, SyntaxKind::Ident]
996 );
997 }
998
999 #[test]
1000 fn lexes_string_name_and_node_path_literals() {
1001 assert_eq!(kinds("emit(&\"died\")\n")[2], SyntaxKind::StringName);
1002 assert_eq!(kinds("var p = ^\"a/b\"\n")[3], SyntaxKind::NodePath);
1003 assert_eq!(kinds("var x = a & b\n")[4], SyntaxKind::Amp);
1005 }
1006
1007 #[test]
1008 fn lexes_number_forms() {
1009 assert_eq!(kinds("1_000_000"), vec![SyntaxKind::Int]);
1010 assert_eq!(kinds("0xfb8c0b"), vec![SyntaxKind::Int]);
1011 assert_eq!(kinds("0b1010_1010"), vec![SyntaxKind::Int]);
1012 assert_eq!(kinds("0.234"), vec![SyntaxKind::Float]);
1013 assert_eq!(kinds("1e-5"), vec![SyntaxKind::Float]);
1014 assert_eq!(kinds("1.5e10"), vec![SyntaxKind::Float]);
1015 assert_eq!(
1017 kinds("1..2"),
1018 vec![SyntaxKind::Int, SyntaxKind::DotDot, SyntaxKind::Int]
1019 );
1020 assert_eq!(
1022 kinds("1.max(2)"),
1023 vec![
1024 SyntaxKind::Int,
1025 SyntaxKind::Dot,
1026 SyntaxKind::Ident,
1027 SyntaxKind::LParen,
1028 SyntaxKind::Int,
1029 SyntaxKind::RParen
1030 ]
1031 );
1032 }
1033
1034 #[test]
1035 fn lexes_string_forms() {
1036 assert_eq!(kinds("\"double\""), vec![SyntaxKind::Str]);
1037 assert_eq!(kinds("'single'"), vec![SyntaxKind::Str]);
1038 assert_eq!(kinds("\"\"\"triple\nspanning\"\"\""), vec![SyntaxKind::Str]);
1039 assert_eq!(kinds("r\"raw\\n\""), vec![SyntaxKind::Str]);
1040 assert_eq!(kinds("\"esc\\\"aped\""), vec![SyntaxKind::Str]);
1041 }
1042
1043 #[test]
1044 fn separates_doc_comments_from_plain_comments() {
1045 let lexed = tokenize("## doc\n# plain\n");
1046 let comments: Vec<_> = lexed
1047 .tokens
1048 .iter()
1049 .filter(|t| t.kind.is_comment())
1050 .map(|t| t.kind)
1051 .collect();
1052 assert_eq!(comments, vec![SyntaxKind::DocComment, SyntaxKind::Comment]);
1053 }
1054
1055 #[test]
1056 fn line_continuation_joins_lines() {
1057 let kinds = kinds("var a = 1 + \\\n\t2\n");
1058 assert!(!kinds.contains(&SyntaxKind::Indent));
1059 assert_eq!(kinds.last(), Some(&SyntaxKind::Int));
1060 }
1061
1062 #[test]
1063 fn reports_inconsistent_dedent() {
1064 let lexed = tokenize("func f():\n\t\tpass\n\treturn\n");
1065 assert!(
1066 lexed
1067 .errors
1068 .iter()
1069 .any(|e| e.message().contains("unindent")),
1070 "expected an unindent diagnostic, got {:?}",
1071 lexed.errors
1072 );
1073 }
1074
1075 #[test]
1076 fn reports_unterminated_string() {
1077 let lexed = tokenize("var s = \"oops\n");
1078 assert!(
1079 lexed
1080 .errors
1081 .iter()
1082 .any(|e| e.message().contains("unterminated"))
1083 );
1084 }
1085
1086 #[test]
1087 fn closes_open_blocks_at_eof() {
1088 let lexed = tokenize("func f():\n\tif a:\n\t\tpass");
1089 let dedents = lexed
1090 .tokens
1091 .iter()
1092 .filter(|t| t.kind == SyntaxKind::Dedent)
1093 .count();
1094 assert_eq!(dedents, 2);
1095 assert_eq!(lexed.tokens.last().map(|t| t.kind), Some(SyntaxKind::Eof));
1096 }
1097}