1use logos::{Lexer, Logos};
13use text_size::{TextRange, TextSize};
14
15use crate::SyntaxKind;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub struct RawToken {
20 pub kind: SyntaxKind,
22 pub range: TextRange,
24}
25
26#[derive(Logos, Debug, Clone, Copy, PartialEq, Eq)]
30enum LexKind {
31 #[token("\u{feff}", priority = 10)]
36 Bom,
37 #[regex(r"[ \t]+")]
38 Whitespace,
39 #[regex(r"\r\n|\n|\r")]
40 NewlinePhys,
41 #[regex(r"\\(\r\n|\n|\r)")]
42 LineContinuation,
43 #[regex(r"#region[^\r\n]*", priority = 5, allow_greedy = true)]
47 RegionComment,
48 #[regex(r"#endregion[^\r\n]*", priority = 5, allow_greedy = true)]
49 EndRegionComment,
50 #[regex(r"##[^\r\n]*", priority = 4, allow_greedy = true)]
51 DocComment,
52 #[regex(r"#[^\r\n]*", priority = 2, allow_greedy = true)]
53 LineComment,
54
55 #[regex(r"0[xX][0-9a-fA-F_]+|0[bB][01_]+|[0-9][0-9_]*")]
57 Int,
58 #[regex(r"[0-9][0-9_]*\.[0-9_]*([eE][+-]?[0-9_]+)?|\.[0-9][0-9_]*([eE][+-]?[0-9_]+)?|[0-9][0-9_]*[eE][+-]?[0-9_]+")]
59 Float,
60 #[token("\"", lex_string)]
64 #[token("'", lex_string)]
65 #[token("\"\"\"", lex_string)]
66 #[token("'''", lex_string)]
67 #[token("r\"", lex_string)]
68 #[token("r'", lex_string)]
69 #[token("r\"\"\"", lex_string)]
70 #[token("r'''", lex_string)]
71 String,
72 #[token("&\"", lex_string)]
73 #[token("&'", lex_string)]
74 StringName,
75 #[token("^\"", lex_string)]
76 #[token("^'", lex_string)]
77 NodePath,
78 #[regex(r"[_\p{XID_Start}]\p{XID_Continue}*")]
83 Ident,
84
85 #[token("(")]
87 LParen,
88 #[token(")")]
89 RParen,
90 #[token("[")]
91 LBrack,
92 #[token("]")]
93 RBrack,
94 #[token("{")]
95 LBrace,
96 #[token("}")]
97 RBrace,
98 #[token(",")]
99 Comma,
100 #[token(":")]
101 Colon,
102 #[token(";")]
103 Semicolon,
104 #[token(".")]
105 Dot,
106 #[token("..")]
107 DotDot,
108 #[token("...")]
109 Ellipsis,
110 #[token("@")]
111 At,
112 #[token("$")]
113 Dollar,
114 #[token("%")]
115 Percent,
116 #[token("&")]
117 Amp,
118 #[token("->")]
119 Arrow,
120 #[token(":=")]
121 ColonEq,
122
123 #[token("+")]
125 Plus,
126 #[token("-")]
127 Minus,
128 #[token("*")]
129 Star,
130 #[token("/")]
131 Slash,
132 #[token("**")]
133 StarStar,
134 #[token("=")]
135 Eq,
136 #[token("==")]
137 EqEq,
138 #[token("!=")]
139 Neq,
140 #[token("<")]
141 Lt,
142 #[token(">")]
143 Gt,
144 #[token("<=")]
145 Le,
146 #[token(">=")]
147 Ge,
148 #[token("&&")]
149 AmpAmp,
150 #[token("||")]
151 PipePipe,
152 #[token("!")]
153 Bang,
154 #[token("~")]
155 Tilde,
156 #[token("|")]
157 Pipe,
158 #[token("^")]
159 Caret,
160 #[token("<<")]
161 Shl,
162 #[token(">>")]
163 Shr,
164 #[token("+=")]
165 PlusEq,
166 #[token("-=")]
167 MinusEq,
168 #[token("*=")]
169 StarEq,
170 #[token("/=")]
171 SlashEq,
172 #[token("**=")]
173 StarStarEq,
174 #[token("%=")]
175 PercentEq,
176 #[token("&=")]
177 AmpEq,
178 #[token("|=")]
179 PipeEq,
180 #[token("^=")]
181 CaretEq,
182 #[token("<<=")]
183 ShlEq,
184 #[token(">>=")]
185 ShrEq,
186}
187
188fn lex_string(lex: &mut Lexer<LexKind>) {
193 let opener = lex.slice().as_bytes();
194 let quote = opener[opener.len() - 1];
195 let triple =
196 opener.len() >= 3 && opener[opener.len() - 2] == quote && opener[opener.len() - 3] == quote;
197
198 let rem = lex.remainder().as_bytes();
199 let n = rem.len();
200 let mut i = 0usize;
201 while i < n {
202 let c = rem[i];
203 if c == b'\\' {
204 i += 2; continue;
206 }
207 if triple {
208 if c == quote && i + 2 < n && rem[i + 1] == quote && rem[i + 2] == quote {
209 i += 3; break;
211 }
212 } else {
213 if c == quote {
214 i += 1; break;
216 }
217 if c == b'\n' || c == b'\r' {
218 break; }
220 }
221 i += 1;
222 }
223 lex.bump(i.min(n));
224}
225
226#[must_use]
230pub fn tokenize(src: &str) -> Vec<RawToken> {
231 let mut out = Vec::new();
232 let mut lexer = LexKind::lexer(src);
233 while let Some(result) = lexer.next() {
234 let span = lexer.span();
235 let kind = match result {
236 Ok(lex_kind) => map_kind(lex_kind, &src[span.clone()]),
237 Err(()) => SyntaxKind::Error,
238 };
239 out.push(RawToken {
240 kind,
241 range: TextRange::new(text_size(span.start), text_size(span.end)),
242 });
243 }
244 out
245}
246
247fn text_size(offset: usize) -> TextSize {
249 TextSize::new(u32::try_from(offset).expect("source files must be smaller than 4 GiB"))
250}
251
252fn map_kind(kind: LexKind, text: &str) -> SyntaxKind {
255 use LexKind as L;
256 use SyntaxKind as S;
257 match kind {
258 L::Bom => S::Bom,
259 L::Whitespace => S::Whitespace,
260 L::NewlinePhys => S::NewlinePhys,
261 L::LineContinuation => S::LineContinuation,
262 L::RegionComment => S::RegionComment,
263 L::EndRegionComment => S::EndRegionComment,
264 L::DocComment => S::DocComment,
265 L::LineComment => S::LineComment,
266 L::Int => S::Int,
267 L::Float => S::Float,
268 L::String => S::String,
269 L::StringName => S::StringName,
270 L::NodePath => S::NodePath,
271 L::Ident => reclassify_ident(text),
272 L::LParen => S::LParen,
273 L::RParen => S::RParen,
274 L::LBrack => S::LBrack,
275 L::RBrack => S::RBrack,
276 L::LBrace => S::LBrace,
277 L::RBrace => S::RBrace,
278 L::Comma => S::Comma,
279 L::Colon => S::Colon,
280 L::Semicolon => S::Semicolon,
281 L::Dot => S::Dot,
282 L::DotDot => S::DotDot,
283 L::Ellipsis => S::Ellipsis,
284 L::At => S::At,
285 L::Dollar => S::Dollar,
286 L::Percent => S::Percent,
287 L::Amp => S::Amp,
288 L::Arrow => S::Arrow,
289 L::ColonEq => S::ColonEq,
290 L::Plus => S::Plus,
291 L::Minus => S::Minus,
292 L::Star => S::Star,
293 L::Slash => S::Slash,
294 L::StarStar => S::StarStar,
295 L::Eq => S::Eq,
296 L::EqEq => S::EqEq,
297 L::Neq => S::Neq,
298 L::Lt => S::Lt,
299 L::Gt => S::Gt,
300 L::Le => S::Le,
301 L::Ge => S::Ge,
302 L::AmpAmp => S::AmpAmp,
303 L::PipePipe => S::PipePipe,
304 L::Bang => S::Bang,
305 L::Tilde => S::Tilde,
306 L::Pipe => S::Pipe,
307 L::Caret => S::Caret,
308 L::Shl => S::Shl,
309 L::Shr => S::Shr,
310 L::PlusEq => S::PlusEq,
311 L::MinusEq => S::MinusEq,
312 L::StarEq => S::StarEq,
313 L::SlashEq => S::SlashEq,
314 L::StarStarEq => S::StarStarEq,
315 L::PercentEq => S::PercentEq,
316 L::AmpEq => S::AmpEq,
317 L::PipeEq => S::PipeEq,
318 L::CaretEq => S::CaretEq,
319 L::ShlEq => S::ShlEq,
320 L::ShrEq => S::ShrEq,
321 }
322}
323
324fn reclassify_ident(text: &str) -> SyntaxKind {
329 use SyntaxKind as S;
330 match text {
331 "if" => S::IfKw,
332 "elif" => S::ElifKw,
333 "else" => S::ElseKw,
334 "for" => S::ForKw,
335 "while" => S::WhileKw,
336 "match" => S::MatchKw,
337 "when" => S::WhenKw,
338 "break" => S::BreakKw,
339 "continue" => S::ContinueKw,
340 "pass" => S::PassKw,
341 "return" => S::ReturnKw,
342 "var" => S::VarKw,
343 "const" => S::ConstKw,
344 "enum" => S::EnumKw,
345 "func" => S::FuncKw,
346 "static" => S::StaticKw,
347 "signal" => S::SignalKw,
348 "class" => S::ClassKw,
349 "class_name" => S::ClassNameKw,
350 "extends" => S::ExtendsKw,
351 "is" => S::IsKw,
352 "in" => S::InKw,
353 "as" => S::AsKw,
354 "self" => S::SelfKw,
355 "super" => S::SuperKw,
356 "void" => S::VoidKw,
357 "await" => S::AwaitKw,
358 "preload" => S::PreloadKw,
359 "assert" => S::AssertKw,
360 "breakpoint" => S::BreakpointKw,
361 "not" => S::NotKw,
362 "and" => S::AndKw,
363 "or" => S::OrKw,
364 "yield" => S::YieldKw,
365 "namespace" => S::NamespaceKw,
366 "trait" => S::TraitKw,
367 "true" => S::True,
368 "false" => S::False,
369 "null" => S::Null,
370 "PI" => S::ConstPi,
371 "TAU" => S::ConstTau,
372 "INF" => S::ConstInf,
373 "NAN" => S::ConstNan,
374 _ => S::Ident,
375 }
376}
377
378#[cfg(test)]
379mod tests {
380 use super::*;
381
382 fn assert_lossless(src: &str) {
384 let toks = tokenize(src);
385 let mut prev_end = TextSize::new(0);
387 let mut rebuilt = String::new();
388 for t in &toks {
389 assert_eq!(
390 t.range.start(),
391 prev_end,
392 "gap/overlap before {t:?} in {src:?}"
393 );
394 prev_end = t.range.end();
395 rebuilt.push_str(&src[t.range]);
396 }
397 assert_eq!(prev_end, TextSize::of(src), "did not cover to EOF: {src:?}");
398 assert_eq!(rebuilt, src, "round-trip mismatch for {src:?}");
399 }
400
401 fn kinds(src: &str) -> Vec<SyntaxKind> {
402 tokenize(src).into_iter().map(|t| t.kind).collect()
403 }
404
405 #[test]
406 fn lossless_over_a_realistic_snippet() {
407 let src = "## doc\n@export var hp: int = 100 # hi\nfunc _ready() -> void:\n\tprint($Player, %Unique)\n";
408 assert_lossless(src);
409 }
410
411 #[test]
412 fn keywords_and_literals_reclassified() {
413 use SyntaxKind as S;
414 assert_eq!(kinds("func"), vec![S::FuncKw]);
415 assert_eq!(
416 kinds("true false null"),
417 vec![S::True, S::Whitespace, S::False, S::Whitespace, S::Null]
418 );
419 assert_eq!(kinds("PI"), vec![S::ConstPi]);
420 assert_eq!(kinds("my_var"), vec![S::Ident]);
421 assert_eq!(kinds("class_name"), vec![S::ClassNameKw]);
422 }
423
424 #[test]
425 fn unicode_identifiers_lex_as_one_ident() {
426 use SyntaxKind as S;
427 assert_eq!(kinds("café"), vec![S::Ident]);
430 assert_eq!(kinds("p\u{0430}ypal"), vec![S::Ident]);
433 }
434
435 #[test]
436 fn numbers() {
437 use SyntaxKind as S;
438 assert_eq!(kinds("0x8f51"), vec![S::Int]);
439 assert_eq!(kinds("0b1010"), vec![S::Int]);
440 assert_eq!(kinds("12_345"), vec![S::Int]);
441 assert_eq!(kinds("3.14"), vec![S::Float]);
442 assert_eq!(kinds(".5"), vec![S::Float]);
443 assert_eq!(kinds("1."), vec![S::Float]);
444 assert_eq!(kinds("58.1e-10"), vec![S::Float]);
445 }
446
447 #[test]
448 fn strings_all_flavours() {
449 use SyntaxKind as S;
450 assert_eq!(kinds(r#""hello""#), vec![S::String]);
451 assert_eq!(kinds("'world'"), vec![S::String]);
452 assert_eq!(kinds(r#""with \" escape""#), vec![S::String]);
453 assert_eq!(kinds(r#"r"raw\n""#), vec![S::String]);
454 assert_eq!(kinds("\"\"\"multi\nline\"\"\""), vec![S::String]);
455 assert_eq!(kinds(r#"&"sname""#), vec![S::StringName]);
456 assert_eq!(kinds(r#"^"node/path""#), vec![S::NodePath]);
457 assert_eq!(kinds(r#"$"Player""#), vec![S::Dollar, S::String]);
459 }
460
461 #[test]
462 fn unterminated_string_is_lossless() {
463 let src = "\"oops\nok";
465 assert_lossless(src);
466 assert_eq!(kinds(src)[0], SyntaxKind::String);
467 assert_lossless("\"\"\"never closed");
469 }
470
471 #[test]
472 fn operators_longest_match() {
473 use SyntaxKind as S;
474 assert_eq!(kinds("**="), vec![S::StarStarEq]);
475 assert_eq!(kinds(">>="), vec![S::ShrEq]);
476 assert_eq!(kinds(":="), vec![S::ColonEq]);
477 assert_eq!(kinds("->"), vec![S::Arrow]);
478 assert_eq!(kinds("..."), vec![S::Ellipsis]);
479 assert_eq!(kinds("&&"), vec![S::AmpAmp]);
480 }
481
482 #[test]
483 fn unlexable_byte_becomes_error_token() {
484 let src = "a ` b";
486 assert_lossless(src);
487 assert!(kinds(src).contains(&SyntaxKind::Error));
488 }
489
490 #[test]
491 fn comments_distinguished() {
492 use SyntaxKind as S;
493 assert_eq!(kinds("# plain"), vec![S::LineComment]);
494 assert_eq!(kinds("## doc"), vec![S::DocComment]);
495 assert_eq!(kinds("#region A"), vec![S::RegionComment]);
496 assert_eq!(kinds("#endregion"), vec![S::EndRegionComment]);
497 }
498}