#![allow(clippy::while_let_loop)]
use crate::span::Span;
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct ScannerState {
pub offset: u32,
}
impl ScannerState {
#[inline]
pub const fn new(offset: u32) -> Self {
Self { offset }
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct Cursor<'src> {
bytes: &'src [u8],
offset: u32,
}
impl<'src> Cursor<'src> {
#[inline]
pub fn new(source: &'src str) -> Self {
Self {
bytes: source.as_bytes(),
offset: 0,
}
}
#[inline]
pub fn offset(&self) -> u32 {
self.offset
}
#[allow(dead_code)]
#[inline]
pub fn len(&self) -> u32 {
self.bytes.len() as u32
}
#[inline]
pub fn is_eof(&self) -> bool {
self.offset as usize >= self.bytes.len()
}
#[inline]
pub fn state(&self) -> ScannerState {
ScannerState::new(self.offset)
}
#[inline]
pub fn checkpoint(&self) -> ScannerState {
self.state()
}
#[inline]
pub fn restore(&mut self, state: ScannerState) {
self.offset = state.offset;
}
#[inline]
pub fn set_offset(&mut self, offset: u32) {
debug_assert!(offset as usize <= self.bytes.len());
self.offset = offset;
}
#[inline]
pub fn peek_byte(&self) -> Option<u8> {
self.bytes.get(self.offset as usize).copied()
}
#[inline]
pub fn peek_byte_at(&self, ahead: u32) -> Option<u8> {
self.bytes.get((self.offset + ahead) as usize).copied()
}
#[inline]
pub fn bump_byte(&mut self) -> Option<u8> {
let b = self.peek_byte()?;
self.offset += 1;
Some(b)
}
#[allow(dead_code)]
pub fn try_eat(&mut self, prefix: &[u8]) -> bool {
let end = self.offset as usize + prefix.len();
if end > self.bytes.len() {
return false;
}
if &self.bytes[self.offset as usize..end] == prefix {
self.offset = end as u32;
true
} else {
false
}
}
pub fn peek_char(&self) -> Option<(char, u32)> {
decode_utf8(self.bytes, self.offset as usize)
}
#[allow(dead_code)]
pub fn bump_char(&mut self) -> Option<(char, u32)> {
let (c, len) = self.peek_char()?;
self.offset += len;
Some((c, len))
}
#[inline]
pub fn span_from(&self, from: u32) -> Span {
Span::new(from, self.offset)
}
}
#[inline]
pub(crate) fn is_ws_byte(b: u8) -> bool {
matches!(b, b' ' | b'\t' | b'\r' | b'\n')
}
#[inline]
pub(crate) fn is_ws_char(c: char) -> bool {
is_ws_byte(c as u32 as u8) && (c as u32) < 0x80 || c as u32 == 0x3000
}
#[inline]
pub(crate) fn is_bidi_char(c: char) -> bool {
matches!(c as u32, 0x061C | 0x200E | 0x200F | 0x2066..=0x2069)
}
#[allow(dead_code)]
#[inline]
pub(crate) fn is_text_char(c: char) -> bool {
let cp = c as u32;
cp != 0 && cp != 0x5C && cp != 0x7B && cp != 0x7D
}
#[allow(dead_code)]
#[inline]
pub(crate) fn is_quoted_char(c: char) -> bool {
let cp = c as u32;
cp != 0 && cp != 0x5C && cp != 0x7C
}
#[allow(dead_code)]
#[inline]
pub(crate) fn is_simple_start_char(c: char) -> bool {
let cp = c as u32;
!matches!(
cp,
0x00 | 0x09 | 0x0A | 0x0D | 0x20 | 0x2E | 0x5C | 0x7B | 0x7D | 0x3000
)
}
#[allow(dead_code)]
#[inline]
pub(crate) fn is_escape_target(b: u8) -> bool {
matches!(b, b'\\' | b'{' | b'|' | b'}')
}
#[allow(dead_code)]
#[inline]
pub(crate) fn ascii_is_name_start(b: u8) -> Option<bool> {
if b >= 0x80 {
return None;
}
Some(matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'_'))
}
#[inline]
pub fn ascii_is_name_char(b: u8) -> Option<bool> {
if b >= 0x80 {
return None;
}
Some(matches!(b, b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'+' | b'_' | b'-' | b'.'))
}
pub(crate) fn is_name_start(c: char) -> bool {
let cp = c as u32;
if cp < 0xA1 {
return matches!(cp, 0x2B | 0x5F | 0x41..=0x5A | 0x61..=0x7A);
}
is_unicode_name_codepoint(cp)
}
pub(crate) fn is_name_char(c: char) -> bool {
let cp = c as u32;
if cp < 0xA1 {
return matches!(
cp,
0x2B | 0x5F | 0x2D | 0x2E | 0x41..=0x5A | 0x61..=0x7A | 0x30..=0x39
);
}
is_unicode_name_codepoint(cp)
}
fn is_unicode_name_codepoint(cp: u32) -> bool {
if cp > 0x0010_FFFD {
return false;
}
if matches!(cp, 0x061C | 0x200E..=0x200F | 0x202A..=0x202E | 0x2066..=0x2069) {
return false;
}
if matches!(
cp,
0x1680 | 0x2000..=0x200A | 0x2028..=0x2029 | 0x202F | 0x205F | 0x3000
) {
return false;
}
if (0xD800..=0xDFFF).contains(&cp) {
return false;
}
if (0xFDD0..=0xFDEF).contains(&cp) {
return false;
}
if cp & 0xFFFE == 0xFFFE {
return false;
}
true
}
#[inline]
pub(crate) fn detect_keyword(cursor: &Cursor<'_>) -> Option<KeywordHit> {
if cursor.peek_byte() != Some(b'.') {
return None;
}
match cursor.peek_byte_at(1) {
Some(b'i') if cursor.try_eat_at_offset(b".input") => Some(KeywordHit::Input),
Some(b'l') if cursor.try_eat_at_offset(b".local") => Some(KeywordHit::Local),
Some(b'm') if cursor.try_eat_at_offset(b".match") => Some(KeywordHit::Match),
_ => None,
}
}
impl Cursor<'_> {
#[inline]
pub(crate) fn try_eat_at_offset(&self, prefix: &[u8]) -> bool {
let end = self.offset as usize + prefix.len();
end <= self.bytes.len() && &self.bytes[self.offset as usize..end] == prefix
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeywordHit {
Input,
Local,
Match,
}
impl KeywordHit {
pub const fn length(self) -> u32 {
match self {
Self::Input | Self::Local | Self::Match => 6,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TriviaMode {
Optional,
Required,
}
#[derive(Debug, Default, Clone, Copy)]
pub(crate) struct PeekTrivia {
pub end_offset: u32,
pub ws_runs: u32,
pub bidi_runs: u32,
pub next_byte: Option<u8>,
}
impl PeekTrivia {
#[inline]
pub fn satisfies_required_s(self) -> bool {
self.ws_runs > 0
}
}
pub(crate) fn peek_trivia(cursor: &Cursor<'_>) -> PeekTrivia {
let mut probe = *cursor;
let mut ws_runs = 0u32;
let mut bidi_runs = 0u32;
loop {
let Some(b) = probe.peek_byte() else {
break;
};
if b < 0x80 {
if is_ws_byte(b) {
probe.set_offset(probe.offset() + 1);
ws_runs += 1;
continue;
}
break;
}
let Some((c, len)) = probe.peek_char() else {
break;
};
if is_ws_char(c) {
probe.set_offset(probe.offset() + len);
ws_runs += 1;
} else if is_bidi_char(c) {
probe.set_offset(probe.offset() + len);
bidi_runs += 1;
} else {
break;
}
}
PeekTrivia {
end_offset: probe.offset(),
ws_runs,
bidi_runs,
next_byte: probe.peek_byte(),
}
}
pub(crate) fn scan_text_run(cursor: &mut Cursor<'_>) -> Span {
let start = cursor.offset();
let haystack = &cursor.bytes[start as usize..];
let stop_delim = memchr::memchr3(b'\\', b'{', b'}', haystack).unwrap_or(haystack.len());
let stop_null = memchr::memchr(0x00, &haystack[..stop_delim]).unwrap_or(stop_delim);
cursor.offset = start + stop_null as u32;
cursor.span_from(start)
}
pub(crate) fn scan_quoted_text_run(cursor: &mut Cursor<'_>) -> Span {
let start = cursor.offset();
let haystack = &cursor.bytes[start as usize..];
let stop_delim = memchr::memchr2(b'\\', b'|', haystack).unwrap_or(haystack.len());
let stop_null = memchr::memchr(0x00, &haystack[..stop_delim]).unwrap_or(stop_delim);
cursor.offset = start + stop_null as u32;
cursor.span_from(start)
}
pub(crate) fn scan_name(cursor: &mut Cursor<'_>) -> Option<Span> {
let start = cursor.offset();
skip_bidi(cursor);
let Some(b0) = cursor.peek_byte() else {
cursor.restore(ScannerState::new(start));
return None;
};
if b0 < 0x80 {
if !matches!(b0, b'A'..=b'Z' | b'a'..=b'z' | b'+' | b'_') {
cursor.restore(ScannerState::new(start));
return None;
}
cursor.offset += 1;
} else {
let saved = cursor.checkpoint();
let Some((c, len)) = cursor.peek_char() else {
cursor.restore(ScannerState::new(start));
return None;
};
if !is_name_start(c) {
cursor.restore(saved);
cursor.set_offset(start);
return None;
}
cursor.offset += len;
}
loop {
let Some(b) = cursor.peek_byte() else { break };
if b < 0x80 {
match ascii_is_name_char(b) {
Some(true) => {
cursor.offset += 1;
}
Some(false) | None => break,
}
} else {
let Some((c, len)) = cursor.peek_char() else {
break;
};
if !is_name_char(c) {
break;
}
cursor.offset += len;
}
}
skip_bidi(cursor);
Some(Span::new(start, cursor.offset()))
}
pub(crate) fn scan_unquoted_literal(cursor: &mut Cursor<'_>) -> Option<Span> {
let start = cursor.offset();
skip_bidi(cursor);
let mut produced = false;
loop {
let Some(b) = cursor.peek_byte() else { break };
if b < 0x80 {
match ascii_is_name_char(b) {
Some(true) => {
cursor.offset += 1;
produced = true;
}
_ => break,
}
} else {
let Some((c, len)) = cursor.peek_char() else {
break;
};
if !is_name_char(c) {
break;
}
cursor.offset += len;
produced = true;
}
}
skip_bidi(cursor);
if produced {
Some(Span::new(start, cursor.offset()))
} else {
cursor.set_offset(start);
None
}
}
fn skip_bidi(cursor: &mut Cursor<'_>) {
loop {
let Some(b) = cursor.peek_byte() else { break };
if b < 0x80 {
break;
}
let Some((c, len)) = cursor.peek_char() else {
break;
};
if !is_bidi_char(c) {
break;
}
cursor.offset += len;
}
}
#[inline]
fn decode_utf8(bytes: &[u8], offset: usize) -> Option<(char, u32)> {
let b0 = *bytes.get(offset)?;
if b0 < 0x80 {
return Some((b0 as char, 1));
}
let (cp, len) = if b0 < 0xC0 {
(0xFFFDu32, 1u32)
} else if b0 < 0xE0 {
let b1 = (*bytes.get(offset + 1)?) as u32;
((u32::from(b0) & 0x1F) << 6 | (b1 & 0x3F), 2)
} else if b0 < 0xF0 {
let b1 = (*bytes.get(offset + 1)?) as u32;
let b2 = (*bytes.get(offset + 2)?) as u32;
(
(u32::from(b0) & 0x0F) << 12 | (b1 & 0x3F) << 6 | (b2 & 0x3F),
3,
)
} else {
let b1 = (*bytes.get(offset + 1)?) as u32;
let b2 = (*bytes.get(offset + 2)?) as u32;
let b3 = (*bytes.get(offset + 3)?) as u32;
(
(u32::from(b0) & 0x07) << 18 | (b1 & 0x3F) << 12 | (b2 & 0x3F) << 6 | (b3 & 0x3F),
4,
)
};
let c = char::from_u32(cp).unwrap_or('\u{FFFD}');
Some((c, len))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cursor_byte_helpers() {
let mut c = Cursor::new("abc");
assert_eq!(c.peek_byte(), Some(b'a'));
assert_eq!(c.peek_byte_at(2), Some(b'c'));
assert_eq!(c.bump_byte(), Some(b'a'));
assert_eq!(c.offset(), 1);
assert!(c.try_eat(b"bc"));
assert!(c.is_eof());
}
#[test]
fn cursor_checkpoint_round_trip() {
let mut c = Cursor::new("xyz");
c.bump_byte();
let saved = c.checkpoint();
c.bump_byte();
c.bump_byte();
assert!(c.is_eof());
c.restore(saved);
assert_eq!(c.offset(), 1);
assert_eq!(c.peek_byte(), Some(b'y'));
}
#[test]
fn peek_char_decodes_utf8() {
let mut c = Cursor::new("aあい"); assert_eq!(c.peek_char(), Some(('a', 1)));
c.bump_char();
assert_eq!(c.peek_char(), Some(('あ', 3)));
c.bump_char();
assert_eq!(c.peek_char(), Some(('い', 3)));
c.bump_char();
assert!(c.is_eof());
}
#[test]
fn ws_and_bidi_predicates_match_spec() {
for b in [b' ', b'\t', b'\r', b'\n'] {
assert!(is_ws_byte(b));
}
assert!(!is_ws_byte(b'.'));
assert!(is_ws_char('\u{3000}'));
for c in ['\u{061C}', '\u{200E}', '\u{200F}', '\u{2066}', '\u{2069}'] {
assert!(is_bidi_char(c));
}
assert!(!is_bidi_char('a'));
}
#[test]
fn text_quoted_simple_start_predicates_match_spec() {
assert!(is_text_char('a'));
assert!(!is_text_char('{'));
assert!(!is_text_char('}'));
assert!(!is_text_char('\\'));
assert!(!is_quoted_char('|'));
assert!(is_quoted_char('a'));
assert!(!is_simple_start_char('.'));
assert!(!is_simple_start_char(' '));
assert!(!is_simple_start_char('\u{3000}'));
assert!(is_simple_start_char('a'));
}
#[test]
fn name_predicates_cover_ascii_fast_path() {
for b in [b'A', b'a', b'Z', b'+', b'_'] {
assert_eq!(ascii_is_name_start(b), Some(true));
}
for b in [b'0', b'-', b'.'] {
assert_eq!(ascii_is_name_start(b), Some(false));
assert_eq!(ascii_is_name_char(b), Some(true));
}
assert_eq!(ascii_is_name_start(b'$'), Some(false));
assert_eq!(ascii_is_name_start(0xFF), None);
assert!(is_name_start('a'));
assert!(is_name_start('あ'));
assert!(!is_name_start('\u{3000}'));
assert!(!is_name_start('\u{061C}')); }
#[test]
fn detect_keyword_returns_match() {
for (input, expected) in [
(".input ", KeywordHit::Input),
(".local foo", KeywordHit::Local),
(".match $x", KeywordHit::Match),
] {
let c = Cursor::new(input);
assert_eq!(detect_keyword(&c), Some(expected));
}
assert!(detect_keyword(&Cursor::new(".other")).is_none());
assert!(detect_keyword(&Cursor::new("input")).is_none());
}
#[test]
fn scan_text_run_stops_at_delimiter() {
let mut c = Cursor::new("Hello{world");
let span = scan_text_run(&mut c);
assert_eq!(span, Span::new(0, 5));
assert_eq!(c.peek_byte(), Some(b'{'));
}
#[test]
fn scan_quoted_text_run_stops_at_pipe() {
let mut c = Cursor::new("abc|tail");
let span = scan_quoted_text_run(&mut c);
assert_eq!(span, Span::new(0, 3));
assert_eq!(c.peek_byte(), Some(b'|'));
}
#[test]
fn scan_name_handles_unicode_and_bidi_wrappers() {
let mut c = Cursor::new("alpha rest");
let span = scan_name(&mut c).unwrap();
assert_eq!(span, Span::new(0, 5));
assert_eq!(c.peek_byte(), Some(b' '));
let mut c = Cursor::new("\u{061C}foo\u{200E} tail");
let span = scan_name(&mut c).unwrap();
assert_eq!(span, Span::new(0, 8));
}
#[test]
fn scan_name_rejects_non_name_start() {
let mut c = Cursor::new("0not-a-name");
assert!(scan_name(&mut c).is_none());
assert_eq!(c.offset(), 0);
}
}