1use std::sync::OnceLock;
10
11use regex::Regex;
12
13use super::decimal::Decimal;
14use super::diagnostics::{ErrorKind, Result, TranslationError};
15use super::frontend_rules::{UnicodeEscape, decode_unicode_escape};
16use super::js_number::js_number_literal;
17use super::{Language, Span};
18
19#[derive(Clone, Debug, Default, PartialEq, Eq)]
21pub struct Source {
22 units: Vec<u16>,
23}
24
25impl Source {
26 #[must_use]
27 pub fn new(text: &str) -> Self {
28 Self {
29 units: text.encode_utf16().collect(),
30 }
31 }
32
33 #[must_use]
34 pub const fn len(&self) -> usize {
35 self.units.len()
36 }
37
38 #[must_use]
39 pub const fn is_empty(&self) -> bool {
40 self.units.is_empty()
41 }
42
43 #[must_use]
44 pub fn units(&self) -> &[u16] {
45 &self.units
46 }
47
48 #[must_use]
50 pub fn at(&self, index: usize) -> Option<u16> {
51 self.units.get(index).copied()
52 }
53
54 #[must_use]
56 pub fn char_at(&self, index: usize) -> Option<char> {
57 self.at(index)
58 .and_then(|unit| char::from_u32(u32::from(unit)))
59 }
60
61 #[must_use]
63 pub fn is_char(&self, index: usize, ch: char) -> bool {
64 self.char_at(index) == Some(ch)
65 }
66
67 #[must_use]
68 pub fn starts_with(&self, pattern: &str, index: usize) -> bool {
69 pattern
70 .encode_utf16()
71 .enumerate()
72 .all(|(offset, unit)| self.at(index + offset) == Some(unit))
73 }
74
75 #[must_use]
77 pub fn slice(&self, start: usize, end: usize) -> String {
78 let end = end.min(self.units.len());
79 let start = start.min(end);
80 String::from_utf16_lossy(&self.units[start..end])
81 }
82
83 #[must_use]
85 pub fn index_of(&self, pattern: &str, from: usize) -> Option<usize> {
86 (from..self.units.len()).find(|&index| self.starts_with(pattern, index))
87 }
88
89 #[must_use]
92 pub fn code_point_text(&self, index: usize) -> String {
93 let high = self.units[index];
94 if (0xD800..0xDC00).contains(&high)
95 && let Some(low) = self.at(index + 1)
96 && (0xDC00..0xE000).contains(&low)
97 {
98 return String::from_utf16_lossy(&self.units[index..index + 2]);
99 }
100 String::from_utf16_lossy(&self.units[index..=index])
101 }
102}
103
104#[derive(Clone, Copy, Debug, PartialEq, Eq)]
105pub enum TokenKind {
106 Number,
107 Identifier,
108 Macro,
109 Punct,
110 String,
111 Interpolation,
112 Template,
113 Eof,
114}
115
116#[derive(Clone, Debug, PartialEq, Eq)]
118pub struct TemplateExpression {
119 pub source: String,
120 pub offset: usize,
121}
122
123#[derive(Clone, Debug, PartialEq, Eq)]
124pub struct TemplatePart {
125 pub text: String,
126 pub expression: Option<TemplateExpression>,
127}
128
129#[derive(Clone, Debug, PartialEq, Eq)]
130pub struct Token {
131 pub kind: TokenKind,
132 pub value: String,
133 pub suffix: String,
135 pub raw: String,
136 pub start: usize,
137 pub end: usize,
138 pub parts: Vec<TemplatePart>,
139 pub line: usize,
141 pub col: usize,
142 pub first: bool,
143}
144
145impl Token {
146 const fn new(kind: TokenKind, value: String, raw: String, start: usize, end: usize) -> Self {
147 Self {
148 kind,
149 value,
150 suffix: String::new(),
151 raw,
152 start,
153 end,
154 parts: Vec::new(),
155 line: 0,
156 col: 0,
157 first: false,
158 }
159 }
160
161 #[must_use]
162 pub const fn span(&self) -> Span {
163 Span {
164 start: self.start,
165 end: self.end,
166 }
167 }
168}
169
170#[derive(Clone, Debug, PartialEq, Eq)]
171pub struct Comment {
172 pub text: String,
173 pub start: usize,
174 pub end: usize,
175}
176
177#[derive(Clone, Debug)]
178pub struct Tokens {
179 pub tokens: Vec<Token>,
180 pub comments: Vec<Comment>,
181}
182
183fn operators(language: Language) -> Vec<&'static str> {
184 let mut list: Vec<&'static str> = match language {
185 Language::JavaScript => vec![
186 ">>>=", "===", "!==", "...", "**=", "<<=", ">>=", ">>>", "&&=", "||=", "??=", "**",
187 "&&", "||", "==", "!=", "<=", ">=", "=>", "+=", "-=", "*=", "/=", "%=", "&=", "|=",
188 "^=", "++", "--", "??", "?.", "<<", ">>",
189 ],
190 Language::Rust => vec![
191 "..=", "::", "->", "=>", "==", "!=", "<=", ">=", "&&", "||", "+=", "-=", "*=", "/=",
192 "%=", "..",
193 ],
194 Language::Lean => vec![
195 ":=", "++", "->", "=>", "==", "!=", "<=", ">=", "&&", "||", "<;>", "<|", "|>", "::",
196 "..", "←", "→", "∀", "∧", "≤", "≥", "≠", "×",
197 ],
198 Language::Rocq => vec![
199 ":=", "=>", "->", "<->", "<=?", "<?", "=?", "<=", ">=", "<>", "/\\", "\\/", "&&", "||",
200 "++", "::", "%",
201 ],
202 };
203 list.sort_by_key(|operator| std::cmp::Reverse(operator.encode_utf16().count()));
206 list
207}
208
209struct CommentSyntax {
210 line: Option<&'static str>,
211 open: &'static str,
212 close: &'static str,
213 nested: bool,
214}
215
216const fn comment_syntax(language: Language) -> CommentSyntax {
217 match language {
218 Language::JavaScript => CommentSyntax {
219 line: Some("//"),
220 open: "/*",
221 close: "*/",
222 nested: false,
223 },
224 Language::Rust => CommentSyntax {
225 line: Some("//"),
226 open: "/*",
227 close: "*/",
228 nested: true,
229 },
230 Language::Lean => CommentSyntax {
231 line: Some("--"),
232 open: "/-",
233 close: "-/",
234 nested: true,
235 },
236 Language::Rocq => CommentSyntax {
237 line: None,
238 open: "(*",
239 close: "*)",
240 nested: true,
241 },
242 }
243}
244
245#[must_use]
247pub const fn is_js_space(unit: u16) -> bool {
248 matches!(
249 unit,
250 0x09..=0x0D
251 | 0x20
252 | 0xA0
253 | 0x1680
254 | 0x2000..=0x200A
255 | 0x2028
256 | 0x2029
257 | 0x202F
258 | 0x205F
259 | 0x3000
260 | 0xFEFF
261 )
262}
263
264fn letter_regex() -> &'static Regex {
265 static LETTER: OnceLock<Regex> = OnceLock::new();
266 LETTER.get_or_init(|| Regex::new(r"^\p{L}$").expect("letter class"))
267}
268
269fn letter_or_number_regex() -> &'static Regex {
270 static LETTER: OnceLock<Regex> = OnceLock::new();
271 LETTER.get_or_init(|| Regex::new(r"^[\p{L}\p{N}]$").expect("letter or number class"))
272}
273
274const LEAN_SYMBOLS: &str = "∀→∧≤≥≠λ×";
275
276fn unicode_test(unit: Option<u16>, regex: &Regex) -> bool {
277 unit.and_then(|unit| char::from_u32(u32::from(unit)))
278 .is_some_and(|ch| !LEAN_SYMBOLS.contains(ch) && regex.is_match(ch.encode_utf8(&mut [0; 4])))
279}
280
281fn is_identifier_start(unit: Option<u16>, language: Language) -> bool {
282 let Some(value) = unit else { return false };
283 if u8::try_from(value).is_ok_and(|byte| byte.is_ascii_alphabetic() || byte == b'_') {
284 return true;
285 }
286 if language == Language::JavaScript && value == u16::from(b'$') {
287 return true;
288 }
289 matches!(language, Language::Lean | Language::Rocq) && unicode_test(unit, letter_regex())
290}
291
292fn is_identifier_part(unit: Option<u16>, language: Language) -> bool {
293 let Some(value) = unit else { return false };
294 if u8::try_from(value).is_ok_and(|byte| byte.is_ascii_alphanumeric() || byte == b'_') {
295 return true;
296 }
297 let is = |ch: u8| value == u16::from(ch);
298 if language == Language::JavaScript && is(b'$') {
299 return true;
300 }
301 if language == Language::Rocq && is(b'\'') {
302 return true;
303 }
304 if language == Language::Lean && (is(b'\'') || is(b'!') || is(b'?')) {
305 return true;
306 }
307 matches!(language, Language::Lean | Language::Rocq)
308 && unicode_test(unit, letter_or_number_regex())
309}
310
311const fn span(start: usize, end: usize) -> Span {
312 Span { start, end }
313}
314
315pub fn tokenize(text: &str, language: Language) -> Result<Tokens> {
322 tokenize_source(&Source::new(text), language)
323}
324
325pub fn tokenize_source(source: &Source, language: Language) -> Result<Tokens> {
331 let operators = operators(language);
332 let comments = comment_syntax(language);
333 let mut tokens: Vec<Token> = Vec::new();
334 let mut comment_list = Vec::new();
335 let mut index = 0;
336 while index < source.len() {
337 let unit = source.units[index];
338 if is_js_space(unit) {
339 index += 1;
340 continue;
341 }
342 if let Some(line) = comments.line
343 && source.starts_with(line, index)
344 {
345 let stop = source.index_of("\n", index).unwrap_or(source.len());
346 comment_list.push(Comment {
347 text: source.slice(index, stop),
348 start: index,
349 end: stop,
350 });
351 index = stop;
352 continue;
353 }
354 if source.starts_with(comments.open, index) {
355 let stop = block_comment_end(source, index, &comments)?;
356 comment_list.push(Comment {
357 text: source.slice(index, stop),
358 start: index,
359 end: stop,
360 });
361 index = stop;
362 continue;
363 }
364 let start = index;
365 let ch = source.char_at(index);
366 if language == Language::JavaScript && ch == Some('`') {
367 let token = template_token(source, index)?;
368 index = token.end;
369 tokens.push(token);
370 continue;
371 }
372 if ch == Some('"') || (language == Language::JavaScript && ch == Some('\'')) {
373 let token = string_token(source, index, language)?;
374 index = token.end;
375 tokens.push(token);
376 continue;
377 }
378 if language == Language::Lean && source.starts_with("s!\"", index) {
379 let mut token = string_token(source, index + 2, language)?;
380 token.kind = TokenKind::Interpolation;
381 token.start = start;
382 token.raw = source.slice(start, token.end);
383 index = token.end;
384 tokens.push(token);
385 continue;
386 }
387 if language == Language::JavaScript
388 && (ch.is_some_and(|ch| ch.is_ascii_digit())
389 || (ch == Some('.')
390 && source
391 .char_at(index + 1)
392 .is_some_and(|ch| ch.is_ascii_digit())))
393 {
394 let token = javascript_number(source, index)?;
395 index = token.end;
396 tokens.push(token);
397 continue;
398 }
399 if ch.is_some_and(|ch| ch.is_ascii_digit()) {
400 let mut stop = index;
401 while source
402 .char_at(stop)
403 .is_some_and(|ch| ch.is_ascii_digit() || ch == '_')
404 {
405 stop += 1;
406 }
407 let digits_end = stop;
408 if source
409 .char_at(stop)
410 .is_some_and(|ch| ch.is_ascii_lowercase())
411 {
412 stop += 1;
413 while source
414 .char_at(stop)
415 .is_some_and(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit())
416 {
417 stop += 1;
418 }
419 }
420 let raw = source.slice(start, stop);
421 let digits = source.slice(start, digits_end);
422 if digits.ends_with('_') {
423 return Err(TranslationError::syntax(
424 format!("malformed number {raw}"),
425 Some(span(start, stop)),
426 ));
427 }
428 let mut token =
429 Token::new(TokenKind::Number, digits.replace('_', ""), raw, start, stop);
430 token.suffix = source.slice(digits_end, stop);
431 tokens.push(token);
432 index = stop;
433 continue;
434 }
435 if is_identifier_start(Some(unit), language) {
436 let mut stop = index + 1;
437 loop {
438 while stop < source.len() && is_identifier_part(source.at(stop), language) {
439 stop += 1;
440 }
441 let qualified = matches!(language, Language::Lean | Language::Rocq)
443 && source.is_char(stop, '.')
444 && stop + 1 < source.len()
445 && is_identifier_start(source.at(stop + 1), language);
446 if !qualified {
447 break;
448 }
449 stop += 2;
450 }
451 if language == Language::Rust
452 && source.is_char(stop, '!')
453 && !source.is_char(stop + 1, '=')
454 {
455 tokens.push(Token::new(
456 TokenKind::Macro,
457 source.slice(index, stop),
458 source.slice(index, stop + 1),
459 start,
460 stop + 1,
461 ));
462 index = stop + 1;
463 continue;
464 }
465 let value = source.slice(index, stop);
466 tokens.push(Token::new(
467 TokenKind::Identifier,
468 value.clone(),
469 value,
470 start,
471 stop,
472 ));
473 index = stop;
474 continue;
475 }
476 let text = operators
477 .iter()
478 .find(|operator| source.starts_with(operator, index))
479 .map_or_else(
480 || source.code_point_text(index),
481 |operator| (*operator).to_owned(),
482 );
483 let length = text.encode_utf16().count();
484 tokens.push(Token::new(
485 TokenKind::Punct,
486 text.clone(),
487 text,
488 start,
489 start + length,
490 ));
491 index += length;
492 }
493 tokens.push(Token::new(
494 TokenKind::Eof,
495 String::new(),
496 String::new(),
497 source.len(),
498 source.len(),
499 ));
500 Ok(Tokens {
501 tokens,
502 comments: comment_list,
503 })
504}
505
506fn javascript_number_regex() -> &'static Regex {
507 static NUMBER: OnceLock<Regex> = OnceLock::new();
508 NUMBER.get_or_init(|| {
509 let run = |digit: &str| format!("{digit}(?:_?{digit})*");
510 let decimal = run("[0-9]");
511 Regex::new(&format!(
512 r"^(?:0[xX]{}|0[oO]{}|0[bB]{}|(?:(?:0|[1-9](?:_?{decimal})?)(?:\.(?:{decimal})?)?|\.{decimal})(?:[eE][+-]?{decimal})?)n?",
513 run("[0-9a-fA-F]"),
514 run("[0-7]"),
515 run("[01]"),
516 ))
517 .expect("JavaScript numeric literal")
518 })
519}
520
521fn javascript_number(source: &Source, index: usize) -> Result<Token> {
525 let rest = source.slice(index, source.len());
526 let found = javascript_number_regex().find(&rest);
527 let raw = found.map_or_else(|| source.slice(index, index + 1), |m| m.as_str().to_owned());
528 let end = index + raw.len();
529 let next = source.char_at(end);
530 let malformed = || {
531 let stop = if end < source.len() { end + 1 } else { end };
532 TranslationError::syntax(
533 format!("malformed number {}", source.slice(index, stop)),
534 Some(span(index, stop)),
535 )
536 };
537 if found.is_none()
539 || next.is_some_and(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '$')
540 {
541 return Err(malformed());
542 }
543 let text = raw.replace('_', "");
544 let token = if let Some(digits) = text.strip_suffix('n') {
545 let hex = digits.starts_with("0x") || digits.starts_with("0X");
546 if !hex && digits.contains(['.', 'e', 'E']) {
547 return Err(malformed());
548 }
549 let value = Decimal::parse(digits).ok_or_else(malformed)?;
550 let mut token = Token::new(TokenKind::Number, value.digits, raw, index, end);
551 token.suffix = String::from("n");
552 token
553 } else {
554 let value = js_number_literal(&text).ok_or_else(malformed)?;
555 Token::new(TokenKind::Number, value, raw, index, end)
556 };
557 Ok(token)
558}
559
560fn block_comment_end(source: &Source, index: usize, comments: &CommentSyntax) -> Result<usize> {
561 let open = comments.open.encode_utf16().count();
562 let close = comments.close.encode_utf16().count();
563 let mut depth = 0usize;
564 let mut cursor = index;
565 while cursor < source.len() {
566 if source.starts_with(comments.open, cursor) {
567 depth += 1;
568 cursor += open;
569 if !comments.nested && depth > 1 {
570 depth = 1;
571 }
572 continue;
573 }
574 if source.starts_with(comments.close, cursor) {
575 depth = depth.saturating_sub(1);
577 cursor += close;
578 if depth == 0 {
579 return Ok(cursor);
580 }
581 continue;
582 }
583 cursor += 1;
584 }
585 Err(TranslationError::syntax(
586 "unterminated block comment",
587 Some(span(index, source.len())),
588 ))
589}
590
591fn string_token(source: &Source, index: usize, language: Language) -> Result<Token> {
592 let quote = source.units[index];
593 let mut cursor = index + 1;
594 let mut value: Vec<u16> = Vec::new();
595 while cursor < source.len() {
596 let unit = source.units[cursor];
597 if unit == quote {
598 if language == Language::Rocq && source.is_char(cursor + 1, '"') {
599 value.push(u16::from(b'"'));
600 cursor += 2;
601 continue;
602 }
603 let mut token = Token::new(
604 TokenKind::String,
605 String::from_utf16_lossy(&value),
606 source.slice(index, cursor + 1),
607 index,
608 cursor + 1,
609 );
610 token.raw = source.slice(index, cursor + 1);
611 return Ok(token);
612 }
613 if unit == u16::from(b'\\') && language != Language::Rocq {
614 let escaped = source.char_at(cursor + 1);
615 if escaped == Some('u') {
616 let units = source.units()[cursor..source.len().min(cursor + 12)]
617 .iter()
618 .map(|unit| f64::from(*unit))
619 .collect();
620 match decode_unicode_escape(units, language == Language::JavaScript) {
621 UnicodeEscape::Scalar(code, end) => {
622 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
625 let character =
626 char::from_u32(code as u32).expect("decoded Unicode scalar");
627 let mut units = [0; 2];
628 value.extend_from_slice(character.encode_utf16(&mut units));
629 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
630 {
631 cursor += end as usize;
632 }
633 continue;
634 }
635 UnicodeEscape::Malformed => {
636 return Err(TranslationError::syntax(
637 "malformed unicode escape",
638 Some(span(cursor, cursor + 2)),
639 ));
640 }
641 UnicodeEscape::Unsupported(end, escape_length) => {
642 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
643 let (end, escape_length) = (end as usize, escape_length as usize);
644 return Err(TranslationError::new(
645 ErrorKind::Unsupported,
646 format!(
647 "unicode escape {} is not a scalar value; Rust strings hold scalar values only",
648 source.slice(cursor, cursor + escape_length)
649 ),
650 Some(span(cursor, cursor + end)),
651 ));
652 }
653 }
654 }
655 let replacement = match escaped {
656 Some('n') => "\n",
657 Some('t') => "\t",
658 Some('r') => "\r",
659 Some('\\') => "\\",
660 Some('"') => "\"",
661 Some('\'') => "'",
662 Some('0') => "\0",
663 Some('{') => "\\{",
664 _ => {
665 return Err(TranslationError::syntax(
666 format!(
667 "unsupported string escape \\{}",
668 escaped_text(source, cursor + 1)
669 ),
670 Some(span(cursor, cursor + 2)),
671 ));
672 }
673 };
674 value.extend(replacement.encode_utf16());
675 cursor += 2;
676 continue;
677 }
678 if unit == u16::from(b'\n') && language == Language::JavaScript {
679 break;
680 }
681 value.push(unit);
682 cursor += 1;
683 }
684 Err(TranslationError::syntax(
685 "unterminated string literal",
686 Some(span(index, source.len())),
687 ))
688}
689
690fn template_token(source: &Source, index: usize) -> Result<Token> {
691 let mut parts = Vec::new();
692 let mut cursor = index + 1;
693 let mut text: Vec<u16> = Vec::new();
694 while cursor < source.len() {
695 let unit = source.units[cursor];
696 if unit == u16::from(b'`') {
697 parts.push(TemplatePart {
698 text: String::from_utf16_lossy(&text),
699 expression: None,
700 });
701 let raw = source.slice(index, cursor + 1);
702 let mut token = Token::new(TokenKind::Template, String::new(), raw, index, cursor + 1);
703 token.parts = parts;
704 return Ok(token);
705 }
706 if unit == u16::from(b'\\') {
707 let replacement = match source.char_at(cursor + 1) {
708 Some('n') => '\n',
709 Some('t') => '\t',
710 Some('\\') => '\\',
711 Some('`') => '`',
712 Some('$') => '$',
713 _ => {
714 return Err(TranslationError::syntax(
715 format!(
716 "unsupported template escape \\{}",
717 escaped_text(source, cursor + 1)
718 ),
719 Some(span(cursor, cursor + 2)),
720 ));
721 }
722 };
723 text.push(replacement as u16);
724 cursor += 2;
725 continue;
726 }
727 if source.starts_with("${", cursor) {
728 let close = matching_brace(source, cursor + 1)?;
729 parts.push(TemplatePart {
730 text: String::from_utf16_lossy(&text),
731 expression: Some(TemplateExpression {
732 source: source.slice(cursor + 2, close),
733 offset: cursor + 2,
734 }),
735 });
736 text.clear();
737 cursor = close + 1;
738 continue;
739 }
740 text.push(unit);
741 cursor += 1;
742 }
743 Err(TranslationError::syntax(
744 "unterminated template literal",
745 Some(span(index, source.len())),
746 ))
747}
748
749fn escaped_text(source: &Source, index: usize) -> String {
751 if index < source.len() {
752 source.slice(index, index + 1)
753 } else {
754 "undefined".to_owned()
755 }
756}
757
758pub fn matching_brace(source: &Source, open: usize) -> Result<usize> {
764 let mut depth = 0i64;
765 for cursor in open..source.len() {
766 if source.is_char(cursor, '{') {
767 depth += 1;
768 }
769 if source.is_char(cursor, '}') {
770 depth -= 1;
771 if depth == 0 {
772 return Ok(cursor);
773 }
774 }
775 }
776 Err(TranslationError::syntax(
777 "unbalanced braces",
778 Some(span(open, source.len())),
779 ))
780}
781
782#[derive(Clone, Debug)]
784pub struct TokenCursor {
785 pub tokens: Vec<Token>,
786 pub language: Language,
787 pub index: usize,
788}
789
790impl TokenCursor {
791 #[must_use]
792 pub const fn new(tokens: Vec<Token>, language: Language) -> Self {
793 Self {
794 tokens,
795 language,
796 index: 0,
797 }
798 }
799
800 #[must_use]
801 pub fn peek(&self) -> &Token {
802 self.peek_at(0)
803 }
804
805 #[must_use]
806 pub fn peek_at(&self, offset: usize) -> &Token {
807 &self.tokens[(self.index + offset).min(self.tokens.len() - 1)]
808 }
809
810 pub fn advance(&mut self) -> Token {
811 let token = self.peek().clone();
812 if token.kind != TokenKind::Eof {
813 self.index += 1;
814 }
815 token
816 }
817
818 #[must_use]
819 pub fn is(&self, value: &str) -> bool {
820 self.is_at(value, 0)
821 }
822
823 #[must_use]
824 pub fn is_at(&self, value: &str, offset: usize) -> bool {
825 let token = self.peek_at(offset);
826 matches!(token.kind, TokenKind::Punct | TokenKind::Identifier) && token.value == value
827 }
828
829 #[must_use]
830 pub fn is_kind(&self, kind: TokenKind) -> bool {
831 self.peek().kind == kind
832 }
833
834 #[must_use]
835 pub fn is_kind_at(&self, kind: TokenKind, offset: usize) -> bool {
836 self.peek_at(offset).kind == kind
837 }
838
839 pub fn eat(&mut self, value: &str) -> Option<Token> {
840 if self.is(value) {
841 Some(self.advance())
842 } else {
843 None
844 }
845 }
846
847 pub fn expect(&mut self, value: &str, context: Option<&str>) -> Result<Token> {
853 if self.is(value) {
854 return Ok(self.advance());
855 }
856 let token = self.peek();
857 Err(TranslationError::syntax(
858 format!(
859 "expected {value}{} but found {}",
860 context.map_or_else(String::new, |context| format!(" in {context}")),
861 describe(token)
862 ),
863 Some(token.span()),
864 ))
865 }
866
867 pub fn identifier(&mut self, context: Option<&str>) -> Result<Token> {
873 let token = self.peek();
874 if token.kind != TokenKind::Identifier {
875 return Err(TranslationError::syntax(
876 format!(
877 "expected identifier{} but found {}",
878 context.map_or_else(String::new, |context| format!(" in {context}")),
879 describe(token)
880 ),
881 Some(token.span()),
882 ));
883 }
884 Ok(self.advance())
885 }
886
887 #[must_use]
888 pub fn at_end(&self) -> bool {
889 self.peek().kind == TokenKind::Eof
890 }
891}
892
893#[must_use]
895pub fn describe(token: &Token) -> String {
896 if token.kind == TokenKind::Eof {
897 "end of input".to_owned()
898 } else {
899 json_string(&token.raw)
900 }
901}
902
903#[must_use]
905pub fn json_string(text: &str) -> String {
906 serde_json::to_string(text).unwrap_or_default()
907}