| use crate::utils::{StrBuf, VecBuf}; | |
| use crate::{DiagCx, SourceFile, Span}; | |
| use core::{ptr, slice}; | |
| use rustc_arena::DroplessArena; | |
| use rustc_lexer::{self as lex, LiteralKind, Token, TokenKind}; | |
| /// A token pattern used for searching and matching by the [`Cursor`]. | |
| /// | |
| /// In the event that a pattern is a multi-token sequence, earlier tokens will be consumed | |
| /// even if the pattern ultimately isn't matched. e.g. With the sequence `:*` matching | |
| /// `DoubleColon` will consume the first `:` and then fail to match, leaving the cursor at | |
| /// the `*`. | |
| #[derive(Clone, Copy)] | |
| pub enum Pat { | |
| /// Matches any number of comments and doc comments. | |
| AnyComments, | |
| CaptureDocLines, | |
| CaptureIdent, | |
| CaptureLifetime, | |
| CaptureLitStr, | |
| AnyIdent, | |
| At, | |
| Bang, | |
| CloseBracket, | |
| CloseParen, | |
| Comma, | |
| DoubleColon, | |
| Eq, | |
| FatArrow, | |
| Gt, | |
| Ident(IdentPat), | |
| Lifetime, | |
| Lit, | |
| LitStr, | |
| Lt, | |
| OpenBrace, | |
| OpenBracket, | |
| OpenParen, | |
| Pound, | |
| Semi, | |
| } | |
| impl Pat { | |
| pub fn desc(self) -> &'static str { | |
| match self { | |
| Self::AnyComments => "comments", | |
| Self::CaptureDocLines => "doc line comments", | |
| Self::AnyIdent | Self::CaptureIdent => "an identifier", | |
| Self::At => "`@`", | |
| Self::Bang => "`!`", | |
| Self::CloseBracket => "`]`", | |
| Self::CloseParen => "`)`", | |
| Self::Comma => "`,`", | |
| Self::DoubleColon => "`::`", | |
| Self::Eq => "`=`", | |
| Self::FatArrow => "`=>`", | |
| Self::Gt => "`>`", | |
| Self::Ident(x) => x.desc(), | |
| Self::Lifetime | Self::CaptureLifetime => "a lifetime", | |
| Self::Lit => "a literal", | |
| Self::LitStr | Self::CaptureLitStr => "a string literal", | |
| Self::Lt => "`<`", | |
| Self::OpenBrace => "`{`", | |
| Self::OpenBracket => "`[`", | |
| Self::OpenParen => "`(`", | |
| Self::Pound => "`#`", | |
| Self::Semi => "`;`", | |
| } | |
| } | |
| } | |
| macro_rules! ident_or_lit { | |
| ($ident:ident) => { | |
| stringify!($ident) | |
| }; | |
| ($_ident:ident $lit:literal) => { | |
| $lit | |
| }; | |
| } | |
| macro_rules! decl_ident_pats { | |
| ($($ident:ident $(= $s:literal)?,)*) => { | |
| #[allow(non_camel_case_types, clippy::upper_case_acronyms)] | |
| #[derive(Clone, Copy)] | |
| pub enum IdentPat { $($ident),* } | |
| impl IdentPat { | |
| pub fn as_str(self) -> &'static str { | |
| match self { $(Self::$ident => ident_or_lit!($ident $($s)?)),* } | |
| } | |
| pub fn desc(self) -> &'static str { | |
| match self { $(Self::$ident => concat!("`", ident_or_lit!($ident $($s)?), "`")),* } | |
| } | |
| } | |
| } | |
| } | |
| decl_ident_pats! { | |
| DEPRECATED, | |
| DEPRECATED_VERSION, | |
| RENAMED, | |
| RENAMED_VERSION, | |
| clippy, | |
| r#pub = "pub", | |
| version, | |
| } | |
| #[derive(Clone, Copy)] | |
| pub struct Capture { | |
| pub pos: u32, | |
| pub len: u32, | |
| } | |
| impl Capture { | |
| pub const EMPTY: Self = Self { pos: 0, len: 0 }; | |
| pub fn mk_sp<'cx>(self, file: &'cx SourceFile<'cx>) -> Span<'cx> { | |
| Span { | |
| file, | |
| range: self.pos..self.pos + self.len, | |
| } | |
| } | |
| } | |
| /// A unidirectional cursor over a token stream that is lexed on demand. | |
| pub struct Cursor<'txt> { | |
| next_token: Token, | |
| pos: u32, | |
| inner: lex::Cursor<'txt>, | |
| text: &'txt str, | |
| } | |
| impl<'txt> Cursor<'txt> { | |
| #[must_use] | |
| pub fn new(text: &'txt str) -> Self { | |
| let mut inner = lex::Cursor::new(text, lex::FrontmatterAllowed::Yes); | |
| Self { | |
| next_token: inner.advance_token(), | |
| pos: 0, | |
| inner, | |
| text, | |
| } | |
| } | |
| /// Gets the text of the captured token assuming it came from this cursor. | |
| #[must_use] | |
| pub fn get_text(&self, capture: Capture) -> &'txt str { | |
| &self.text[capture.pos as usize..(capture.pos + capture.len) as usize] | |
| } | |
| /// Gets the text that makes up the next token in the stream, or the empty string if | |
| /// stream is exhausted. | |
| #[must_use] | |
| pub fn peek_text(&self) -> &'txt str { | |
| &self.text[self.pos as usize..(self.pos + self.next_token.len) as usize] | |
| } | |
| /// Gets the length of the next token in bytes, or zero if the stream is exhausted. | |
| #[must_use] | |
| pub fn peek_len(&self) -> u32 { | |
| self.next_token.len | |
| } | |
| /// Gets the next token in the stream, or [`TokenKind::Eof`] if the stream is | |
| /// exhausted. | |
| #[must_use] | |
| pub fn peek(&self) -> TokenKind { | |
| self.next_token.kind | |
| } | |
| /// Gets the offset of the next token in the source string, or the string's length if | |
| /// the stream is exhausted. | |
| #[must_use] | |
| pub fn pos(&self) -> u32 { | |
| self.pos | |
| } | |
| /// Advances the cursor to the next token. If the stream is exhausted this will set | |
| /// the next token to [`TokenKind::Eof`]. | |
| pub fn step(&mut self) { | |
| // `next_token.len` is zero for the eof marker. | |
| self.pos += self.next_token.len; | |
| self.next_token = self.inner.advance_token(); | |
| } | |
| #[track_caller] | |
| pub fn emit_unexpected<'cx>(&self, dcx: &mut DiagCx, file: &'cx SourceFile<'cx>, expected: &'static str) { | |
| let sp = Span::new(file, self.pos..self.pos + self.next_token.len); | |
| let msg = if matches!(self.next_token.kind, TokenKind::Eof) { | |
| "end of file" | |
| } else { | |
| "token" | |
| }; | |
| dcx.emit_spanned_err(sp, format!("unexpected {msg}, expected {expected}")); | |
| } | |
| /// Consumes tokens until the given pattern is either fully matched of fails to match. | |
| /// Returns whether the pattern was fully matched. | |
| /// | |
| /// For each capture made by the pattern one item will be taken from the capture | |
| /// sequence with the result placed inside. | |
| fn match_impl(&mut self, pat: Pat, captures: &mut slice::IterMut<'_, Capture>) -> bool { | |
| loop { | |
| match (pat, self.next_token.kind) { | |
| #[rustfmt::skip] // rustfmt bug: https://github.com/rust-lang/rustfmt/issues/6697 | |
| (_, TokenKind::Whitespace) | |
| | ( | |
| Pat::AnyComments, | |
| TokenKind::BlockComment { terminated: true, .. } | TokenKind::LineComment { .. }, | |
| ) => self.step(), | |
| (Pat::AnyComments, _) => return true, | |
| (Pat::Ident(x), TokenKind::Ident) if x.as_str() == self.peek_text() => break, | |
| (Pat::Lit, TokenKind::Ident) if matches!(self.peek_text(), "true" | "false") => break, | |
| (Pat::At, TokenKind::At) | |
| | (Pat::AnyIdent, TokenKind::Ident | TokenKind::RawIdent) | |
| | (Pat::Bang, TokenKind::Bang) | |
| | (Pat::CloseBracket, TokenKind::CloseBracket) | |
| | (Pat::CloseParen, TokenKind::CloseParen) | |
| | (Pat::Comma, TokenKind::Comma) | |
| | (Pat::Eq, TokenKind::Eq) | |
| | (Pat::Lifetime, TokenKind::Lifetime { .. }) | |
| | (Pat::Lit, TokenKind::Literal { .. }) | |
| | (Pat::Lt, TokenKind::Lt) | |
| | (Pat::Gt, TokenKind::Gt) | |
| | (Pat::OpenBrace, TokenKind::OpenBrace) | |
| | (Pat::OpenBracket, TokenKind::OpenBracket) | |
| | (Pat::OpenParen, TokenKind::OpenParen) | |
| | (Pat::Pound, TokenKind::Pound) | |
| | (Pat::Semi, TokenKind::Semi) | |
| | ( | |
| Pat::LitStr, | |
| TokenKind::Literal { | |
| kind: LiteralKind::Str { terminated: true } | LiteralKind::RawStr { .. }, | |
| .. | |
| }, | |
| ) => break, | |
| (Pat::DoubleColon, TokenKind::Colon) if self.inner.as_str().starts_with(':') => { | |
| self.step(); | |
| break; | |
| }, | |
| (Pat::FatArrow, TokenKind::Eq) if self.inner.as_str().starts_with('>') => { | |
| self.step(); | |
| break; | |
| }, | |
| #[rustfmt::skip] | |
| ( | |
| Pat::CaptureLitStr, | |
| TokenKind::Literal { | |
| kind: | |
| LiteralKind::Str { terminated: true } | |
| | LiteralKind::RawStr { n_hashes: Some(_) }, | |
| .. | |
| }, | |
| ) | |
| | (Pat::CaptureIdent, TokenKind::Ident) | |
| | (Pat::CaptureLifetime, TokenKind::Lifetime { .. }) => { | |
| *captures.next().unwrap() = Capture { pos: self.pos, len: self.next_token.len }; | |
| self.step(); | |
| return true; | |
| }, | |
| (Pat::CaptureDocLines, TokenKind::LineComment { doc_style: Some(_) }) => { | |
| let pos = self.pos; | |
| loop { | |
| self.step(); | |
| if !matches!( | |
| self.next_token.kind, | |
| TokenKind::Whitespace | TokenKind::LineComment { doc_style: Some(_) } | |
| ) { | |
| break; | |
| } | |
| } | |
| *captures.next().unwrap() = Capture { | |
| pos, | |
| len: self.pos - pos, | |
| }; | |
| return true; | |
| }, | |
| (Pat::CaptureDocLines, _) => { | |
| *captures.next().unwrap() = Capture::EMPTY; | |
| return true; | |
| }, | |
| _ => return false, | |
| } | |
| } | |
| self.step(); | |
| true | |
| } | |
| /// Consumes and captures the next non-whitespace token if it's an identifier. Returns | |
| /// `None` otherwise. | |
| #[must_use] | |
| pub fn capture_ident(&mut self) -> Option<Capture> { | |
| loop { | |
| match self.next_token.kind { | |
| TokenKind::Whitespace => self.step(), | |
| TokenKind::Ident => { | |
| let res = Capture { | |
| pos: self.pos, | |
| len: self.next_token.len, | |
| }; | |
| self.step(); | |
| return Some(res); | |
| }, | |
| _ => return None, | |
| } | |
| } | |
| } | |
| /// Consumes all tokens up to and including the next identifier. Returns either the | |
| /// captured identifier or `None` if one was not found. | |
| #[must_use] | |
| pub fn find_capture_ident(&mut self) -> Option<Capture> { | |
| loop { | |
| match self.next_token.kind { | |
| TokenKind::Eof => return None, | |
| TokenKind::Ident => { | |
| let res = Capture { | |
| pos: self.pos, | |
| len: self.next_token.len, | |
| }; | |
| self.step(); | |
| return Some(res); | |
| }, | |
| _ => self.step(), | |
| } | |
| } | |
| } | |
| /// Consumes and captures the text of a path without any internal whitespace. Returns | |
| /// `Err` if the path ends with `::`, and `None` if no path component exists at the | |
| /// current position. | |
| /// | |
| /// Only paths containing identifiers separated by `::` with a possible leading `::`. | |
| /// Generic arguments and qualified paths are not considered. | |
| pub fn capture_opt_path( | |
| &mut self, | |
| buf: &mut StrBuf, | |
| arena: &'txt DroplessArena, | |
| ) -> Result<Option<&'txt str>, &'static str> { | |
| #[derive(Clone, Copy)] | |
| enum State { | |
| Start, | |
| Sep, | |
| Ident, | |
| } | |
| buf.with(|buf| { | |
| let start = self.pos; | |
| let mut state = State::Start; | |
| loop { | |
| match (state, self.next_token.kind) { | |
| (_, TokenKind::Whitespace) => self.step(), | |
| (State::Start | State::Ident, TokenKind::Colon) if self.inner.first() == ':' => { | |
| state = State::Sep; | |
| buf.push_str("::"); | |
| self.step(); | |
| self.step(); | |
| }, | |
| (State::Start | State::Sep, TokenKind::Ident) => { | |
| state = State::Ident; | |
| buf.push_str(self.peek_text()); | |
| self.step(); | |
| }, | |
| (State::Ident, _) => break, | |
| (State::Start, _) => return Ok(None), | |
| (State::Sep, _) => return Err(Pat::AnyIdent.desc()), | |
| } | |
| } | |
| let text = self.text[start as usize..self.pos as usize].trim(); | |
| Ok(Some(if text.len() == buf.len() { | |
| text | |
| } else { | |
| arena.alloc_str(buf) | |
| })) | |
| }) | |
| } | |
| pub fn eat_list(&mut self, mut f: impl FnMut(&mut Self) -> Result<bool, &'static str>) -> Result<(), &'static str> { | |
| while f(self)? { | |
| if !self.eat_comma() { | |
| break; | |
| } | |
| } | |
| Ok(()) | |
| } | |
| pub fn capture_list<'cx, T: Copy>( | |
| &mut self, | |
| buf: &mut VecBuf<T>, | |
| arena: &'cx DroplessArena, | |
| mut f: impl FnMut(&mut Self) -> Result<Option<T>, &'static str>, | |
| ) -> Result<&'cx mut [T], &'static str> { | |
| buf.with(|buf| { | |
| while let Some(x) = f(self)? { | |
| buf.push(x); | |
| if !self.eat_comma() { | |
| break; | |
| } | |
| } | |
| Ok(if buf.is_empty() { | |
| &mut [] | |
| } else { | |
| arena.alloc_slice(buf) | |
| }) | |
| }) | |
| } | |
| /// Attempts to match a sequence of patterns at the current position. Returns whether | |
| /// all patterns were successfully matched. | |
| /// | |
| /// Captures will be written to the given slice in the order they're matched. If a | |
| /// capture is matched, but there are no more capture slots this will panic. If the | |
| /// match is completed without filling all the capture slots they will be left | |
| /// unmodified. | |
| /// | |
| /// If the match fails the cursor will be positioned at the first failing token. | |
| pub fn match_all(&mut self, pats: &[Pat], captures: &mut [Capture]) -> Result<(), &'static str> { | |
| let mut captures = captures.iter_mut(); | |
| pats.iter() | |
| .try_for_each(|&pat| self.match_impl(pat, &mut captures).ok_or(pat.desc())) | |
| } | |
| /// Attempts to match a sequence of patterns at the current position. Returns whether | |
| /// all patterns were successfully matched. | |
| /// | |
| /// Captures will be written to the given slice in the order they're matched. If a | |
| /// capture is matched, but there are no more capture slots this will panic. If the | |
| /// match is completed without filling all the capture slots they will be left | |
| /// unmodified. | |
| /// | |
| /// If the match fails the cursor will be positioned at the first failing token. | |
| pub fn opt_match_all(&mut self, pats: &[Pat], captures: &mut [Capture]) -> Result<bool, &'static str> { | |
| let mut captures = captures.iter_mut(); | |
| match pats | |
| .iter() | |
| .try_for_each(|p| self.match_impl(*p, &mut captures).ok_or(p)) | |
| { | |
| Ok(()) => Ok(true), | |
| Err(p) if ptr::addr_eq(pats.as_ptr(), p) => Ok(false), | |
| Err(p) => Err(p.desc()), | |
| } | |
| } | |
| } | |
| macro_rules! mk_tk_methods { | |
| ($( | |
| [$desc:literal] | |
| $name:ident(&mut $self:tt $($params:tt)*) | |
| { $pat:pat $(if $guard:expr)? $(=> $extra:block)? } | |
| )*) => { | |
| #[allow(dead_code)] | |
| impl Cursor<'_> {$( | |
| #[doc = "Consumes the next non-whitespace token if it's "] | |
| #[doc = $desc] | |
| #[doc = " and returns whether the token was found."] | |
| #[must_use] | |
| pub fn ${concat(eat_, $name)}(&mut $self $($params)*) -> bool { | |
| loop { | |
| match $self.next_token.kind { | |
| TokenKind::Whitespace => $self.step(), | |
| $pat $(if $guard)? => { | |
| $self.step(); | |
| return true; | |
| }, | |
| _ => return false, | |
| } | |
| } | |
| } | |
| #[doc = "Consumes all tokens up to and including "] | |
| #[doc = $desc] | |
| #[doc = " and returns whether the token was found."] | |
| #[must_use] | |
| pub fn ${concat(find_, $name)}(&mut $self $($params)*) -> bool { | |
| loop { | |
| match $self.next_token.kind { | |
| TokenKind::Eof => return false, | |
| $pat $(if $guard)? => { | |
| $self.step(); | |
| return true; | |
| }, | |
| _ => $self.step(), | |
| } | |
| } | |
| } | |
| )*} | |
| } | |
| } | |
| mk_tk_methods! { | |
| ["`!`"] | |
| bang(&mut self) { TokenKind::Bang } | |
| ["`}`"] | |
| close_brace(&mut self) { TokenKind::CloseBrace } | |
| ["`]`"] | |
| close_bracket(&mut self) { TokenKind::CloseBracket } | |
| ["`,`"] | |
| comma(&mut self) { TokenKind::Comma } | |
| ["`::`"] | |
| double_colon(&mut self) { | |
| TokenKind::Colon if self.inner.as_str().starts_with(':') => { self.step(); } | |
| } | |
| ["the specified identifier"] | |
| ident(&mut self, s: &str) { TokenKind::Ident if self.peek_text() == s } | |
| ["`;`"] | |
| semi(&mut self) { TokenKind::Semi } | |
| } |