use crate::error::ParseError;
use crate::formats::int_cap::{MAX_INT_DIGITS, over_cap_message};
use crate::schema::{
canonicalize_iso_datetime, canonicalize_iso_time, is_iso_date, is_iso_datetime, is_iso_time,
};
#[derive(Debug, Clone, PartialEq)]
pub(super) enum TokKind {
Sep,
Eof,
LBrace,
RBrace,
LBracket,
RBracket,
Comma,
Colon,
Str(String),
Temporal(TemporalKind, String),
Ident(String),
Int(num_bigint::BigInt),
Float(f64),
}
pub(super) struct Scanner<'a> {
pub(super) text: &'a str,
n: usize,
pub(super) pos: usize,
}
impl<'a> Scanner<'a> {
pub(super) fn new(text: &'a str) -> Self {
let text = text.strip_prefix('\u{feff}').unwrap_or(text);
let n = text.len();
Scanner { text, n, pos: 0 }
}
fn char_at(&self, at: usize) -> Option<char> {
self.text.get(at..)?.chars().next()
}
fn byte_at(&self, at: usize) -> Option<u8> {
self.text.as_bytes().get(at).copied()
}
pub(super) fn line_col(&self, pos: usize) -> (usize, usize) {
let bytes = self.text.as_bytes();
let mut line = 1usize;
let mut last_nl: Option<usize> = None;
for (i, &b) in bytes[..pos].iter().enumerate() {
if b == b'\n' {
line += 1;
last_nl = Some(i);
}
}
let col = match last_nl {
Some(i) => pos - i,
None => pos + 1,
};
(line, col)
}
pub(super) fn error_at(&self, pos: usize, msg: String) -> ParseError {
let (line, col) = self.line_col(pos);
ParseError::new(line, col, msg)
}
fn word_boundary_ok(&self, end: usize) -> bool {
!self
.byte_at(end)
.is_some_and(|b| b.is_ascii_alphanumeric() || b == b'-')
}
pub(super) fn next(&mut self) -> Result<(TokKind, usize, usize), ParseError> {
loop {
let start = self.pos;
if self.consume_ws_or_comment_run() {
continue;
}
if self.pos > start {
return Ok((TokKind::Sep, start, self.pos));
}
if self.pos >= self.n {
return Ok((TokKind::Eof, self.pos, self.pos));
}
let c = self
.char_at(self.pos)
.expect("just checked self.pos < self.n above");
return match c {
'"' => self.scan_dquote_family(start),
'\'' => self.scan_raw_string(start),
'{' => self.single(TokKind::LBrace, start),
'}' => self.single(TokKind::RBrace, start),
'[' => self.single(TokKind::LBracket, start),
']' => self.single(TokKind::RBracket, start),
',' => self.single(TokKind::Comma, start),
':' => self.single(TokKind::Colon, start),
'-' => self.scan_minus(start),
c if c.is_ascii_digit() => self.scan_digit_start(start),
c if c.is_alphabetic() || c == '_' => self.scan_word(start),
other => Err(self.error_at(start, format!("stray character {other:?}"))),
};
}
}
fn single(
&mut self,
kind: TokKind,
start: usize,
) -> Result<(TokKind, usize, usize), ParseError> {
self.pos = start + 1;
Ok((kind, start, self.pos))
}
fn consume_ws_or_comment_run(&mut self) -> bool {
let start = self.pos;
let mut saw_sep = false;
loop {
match self.char_at(self.pos) {
Some(' ') | Some('\t') => self.pos += 1,
Some('#') => {
while let Some(c) = self.char_at(self.pos) {
if c == '\n' {
break;
}
self.pos += c.len_utf8();
}
}
Some('\r') if self.char_at(self.pos + 1) == Some('\n') => {
self.pos += 2;
saw_sep = true;
}
Some('\n') => {
self.pos += 1;
saw_sep = true;
}
Some(';') => {
self.pos += 1;
saw_sep = true;
}
_ => break,
}
}
self.pos > start && !saw_sep
}
fn scan_dquote_family(&mut self, start: usize) -> Result<(TokKind, usize, usize), ParseError> {
if self.char_at(start + 1) == Some('"') && self.char_at(start + 2) == Some('"') {
self.scan_multiline(start)
} else {
self.scan_dquote(start)
}
}
fn scan_dquote(&mut self, start: usize) -> Result<(TokKind, usize, usize), ParseError> {
let mut i = start + 1;
let mut out = String::new();
loop {
match self.char_at(i) {
None => {
return Err(self.error_at(
start,
"unterminated string (missing closing \")".to_string(),
));
}
Some('"') => {
i += 1;
self.pos = i;
return Ok((TokKind::Str(out), start, i));
}
Some('\\') => {
let (ch, next_i) = self.decode_escape(start, i)?;
out.push_str(&ch);
i = next_i;
}
Some(c) if (c as u32) < 0x20 => {
return Err(self.error_at(
start,
format!("control character U+{:04X} in string", c as u32),
));
}
Some(c) => {
out.push(c);
i += c.len_utf8();
}
}
}
}
fn scan_multiline(&mut self, start: usize) -> Result<(TokKind, usize, usize), ParseError> {
let mut i = start + 3;
if self.char_at(i) == Some('\n') {
i += 1;
} else if self.char_at(i) == Some('\r') && self.char_at(i + 1) == Some('\n') {
i += 2;
}
let mut out = String::new();
loop {
match self.char_at(i) {
None => {
return Err(self.error_at(
start,
"unterminated multiline string (missing closing \"\"\")".to_string(),
));
}
Some('"') => {
let mut run = 0usize;
let mut j = i;
while self.char_at(j) == Some('"') {
run += 1;
j += 1;
}
if run >= 3 {
for _ in 0..(run - 3) {
out.push('"');
}
i += run;
self.pos = i;
return Ok((TokKind::Str(out), start, i));
}
for _ in 0..run {
out.push('"');
}
i = j;
}
Some('\\') => {
let (ch, next_i) = self.decode_escape(start, i)?;
out.push_str(&ch);
i = next_i;
}
Some(c) if c == '\t' || c == '\n' || (c as u32) >= 0x20 => {
out.push(c);
i += c.len_utf8();
}
Some(c) => {
return Err(self.error_at(
start,
format!("control character U+{:04X} in multiline string", c as u32),
));
}
}
}
}
fn scan_raw_string(&mut self, start: usize) -> Result<(TokKind, usize, usize), ParseError> {
let mut i = start + 1;
loop {
match self.char_at(i) {
None => {
return Err(self.error_at(
start,
"unterminated raw string (missing closing ')".to_string(),
));
}
Some('\'') => {
let text: String = self.text[start + 1..i].to_string();
i += 1;
self.pos = i;
return Ok((TokKind::Str(text), start, i));
}
Some(c) => i += c.len_utf8(),
}
}
}
fn decode_escape(&self, tok_start: usize, i: usize) -> Result<(String, usize), ParseError> {
let Some(c) = self.char_at(i + 1) else {
return Err(self.error_at(tok_start, "unterminated escape sequence".to_string()));
};
let simple = match c {
'"' => Some('"'),
'\\' => Some('\\'),
'/' => Some('/'),
'b' => Some('\u{8}'),
'f' => Some('\u{c}'),
'n' => Some('\n'),
'r' => Some('\r'),
't' => Some('\t'),
_ => None,
};
if let Some(ch) = simple {
return Ok((ch.to_string(), i + 2));
}
if c != 'u' {
return Err(self.error_at(tok_start, format!("invalid escape \\{c}")));
}
let cp = self.read_hex4(tok_start, i + 2)?;
let j = i + 6;
if (0xD800..=0xDBFF).contains(&cp) {
let has_low_escape = self.char_at(j) == Some('\\') && self.char_at(j + 1) == Some('u');
let low = if has_low_escape {
Some(self.read_hex4(tok_start, j + 2)?)
} else {
None
};
let unpaired_err = || {
self.error_at(
tok_start,
format!(
"unpaired high surrogate \\u{cp:04x} (needs a following low-surrogate \
\\uDC00-\\uDFFF escape)"
),
)
};
return match low {
Some(low) if (0xDC00..=0xDFFF).contains(&low) => {
let combined = 0x10000 + (cp - 0xD800) * 0x400 + (low - 0xDC00);
let ch = char::from_u32(combined)
.expect("surrogate pair math always yields a valid scalar value");
Ok((ch.to_string(), j + 6))
}
_ => Err(unpaired_err()),
};
}
if (0xDC00..=0xDFFF).contains(&cp) {
return Err(self.error_at(tok_start, format!("unpaired low surrogate \\u{cp:04x}")));
}
let ch = char::from_u32(cp)
.expect("cp outside the surrogate range is always a valid scalar value");
Ok((ch.to_string(), j))
}
fn read_hex4(&self, tok_start: usize, at: usize) -> Result<u32, ParseError> {
let hex: String = (0..4)
.map(|k| self.char_at(at + k))
.collect::<Option<Vec<char>>>()
.unwrap_or_default()
.into_iter()
.collect();
if hex.len() != 4 || !hex.chars().all(|c| c.is_ascii_hexdigit()) {
return Err(self.error_at(
tok_start,
r"invalid \u escape (need 4 hex digits)".to_string(),
));
}
Ok(u32::from_str_radix(&hex, 16).expect("validated 4 hex digits"))
}
fn scan_minus(&mut self, start: usize) -> Result<(TokKind, usize, usize), ParseError> {
if self.matches_word(start, "-inf") && self.word_boundary_ok(start + 4) {
self.pos = start + 4;
return Ok((TokKind::Float(f64::NEG_INFINITY), start, self.pos));
}
if self.char_at(start + 1).is_some_and(|c| c.is_ascii_digit()) {
return self.scan_number(start);
}
Err(self.error_at(start, "stray character '-'".to_string()))
}
fn scan_digit_start(&mut self, start: usize) -> Result<(TokKind, usize, usize), ParseError> {
if let Some(end) = self.try_datetime(start) {
return self.finish_temporal(start, end, TemporalKind::Datetime);
}
if let Some(end) = self.try_date(start) {
return self.finish_temporal(start, end, TemporalKind::Date);
}
if let Some(end) = self.try_time(start) {
return self.finish_temporal(start, end, TemporalKind::Time);
}
self.scan_number(start)
}
fn finish_temporal(
&mut self,
start: usize,
end: usize,
kind: TemporalKind,
) -> Result<(TokKind, usize, usize), ParseError> {
let text: String = self.text[start..end].to_string();
let valid = match kind {
TemporalKind::Date => is_iso_date(&text),
TemporalKind::Time => is_iso_time(&text),
TemporalKind::Datetime => is_iso_datetime(&text),
};
if !valid {
let label = match kind {
TemporalKind::Date => "date",
TemporalKind::Time => "time",
TemporalKind::Datetime => "datetime",
};
return Err(self.error_at(end, format!("invalid {label} {text:?}")));
}
self.pos = end;
let canonical = match kind {
TemporalKind::Date => text,
TemporalKind::Time => canonicalize_iso_time(&text),
TemporalKind::Datetime => canonicalize_iso_datetime(&text),
};
Ok((TokKind::Temporal(kind, canonical), start, end))
}
fn digits_from(&self, pos: usize) -> usize {
let mut p = pos;
while self.byte_at(p).is_some_and(|b| b.is_ascii_digit()) {
p += 1;
}
p
}
fn expect_digits(&self, pos: usize, count: usize) -> Option<usize> {
let end = self.digits_from(pos);
if end - pos == count { Some(end) } else { None }
}
fn try_date(&self, pos: usize) -> Option<usize> {
let mut p = self.expect_digits(pos, 4)?;
if self.byte_at(p) != Some(b'-') {
return None;
}
p = self.expect_digits(p + 1, 2)?;
if self.byte_at(p) != Some(b'-') {
return None;
}
self.expect_digits(p + 1, 2)
}
fn try_time(&self, pos: usize) -> Option<usize> {
let mut p = self.expect_digits(pos, 2)?;
if self.byte_at(p) != Some(b':') {
return None;
}
p = self.expect_digits(p + 1, 2)?;
if self.byte_at(p) == Some(b':')
&& let Some(after_secs) = self.expect_digits(p + 1, 2)
{
p = after_secs;
if self.byte_at(p) == Some(b'.') {
let frac_end = self.digits_from(p + 1);
if frac_end > p + 1 && frac_end - (p + 1) <= 6 {
p = frac_end;
}
}
}
if let Some(sign) = self.byte_at(p)
&& (sign == b'+' || sign == b'-')
&& let Some(after_h) = self.expect_digits(p + 1, 2)
&& self.byte_at(after_h) == Some(b':')
&& let Some(after_m) = self.expect_digits(after_h + 1, 2)
{
p = after_m;
}
Some(p)
}
fn try_datetime(&self, pos: usize) -> Option<usize> {
let d_end = self.try_date(pos)?;
if self.byte_at(d_end) != Some(b'T') {
return None;
}
self.try_time(d_end + 1)
}
fn scan_number(&mut self, start: usize) -> Result<(TokKind, usize, usize), ParseError> {
let mut p = start;
if self.byte_at(p) == Some(b'-') {
p += 1;
}
let int_start = p;
p = self.digits_from(p);
debug_assert!(p > int_start, "caller guarantees at least one digit");
let mut end = p;
let mut is_float = false;
let frac_end = if self.byte_at(p) == Some(b'.') {
self.digits_from(p + 1)
} else {
p
};
if self.byte_at(p) == Some(b'.') && frac_end > p + 1 {
p = frac_end;
is_float = true;
end = self.try_exponent(p).unwrap_or(p);
} else if let Some(e) = self.try_exponent(p) {
end = e;
is_float = true;
}
let text: &str = &self.text[start..end];
self.pos = end;
if is_float {
let v: f64 = text
.parse()
.expect("scanner only emits float-shaped digit/exponent text, which f64::from_str always parses");
Ok((TokKind::Float(v), start, end))
} else {
let digits = &text[if text.starts_with('-') { 1 } else { 0 }..];
if digits.len() > MAX_INT_DIGITS {
return Err(self.error_at(start, over_cap_message("", digits.len())));
}
let v = num_bigint::BigInt::parse_bytes(text.as_bytes(), 10).expect(
"scanner only emits digit-shaped text, which BigInt::parse_bytes always parses",
);
Ok((TokKind::Int(v), start, end))
}
}
fn try_exponent(&self, pos: usize) -> Option<usize> {
if !matches!(self.byte_at(pos), Some(b'e') | Some(b'E')) {
return None;
}
let mut q = pos + 1;
if matches!(self.byte_at(q), Some(b'+') | Some(b'-')) {
q += 1;
}
let dstart = q;
q = self.digits_from(q);
if q > dstart { Some(q) } else { None }
}
fn matches_word(&self, pos: usize, word: &str) -> bool {
debug_assert!(
word.is_ascii(),
"matches_word is only used with ASCII keywords"
);
word.bytes()
.enumerate()
.all(|(i, b)| self.byte_at(pos + i) == Some(b))
}
fn scan_word(&mut self, start: usize) -> Result<(TokKind, usize, usize), ParseError> {
if self.matches_word(start, "nan") && self.word_boundary_ok(start + 3) {
self.pos = start + 3;
return Ok((TokKind::Float(f64::NAN), start, self.pos));
}
if self.matches_word(start, "inf") && self.word_boundary_ok(start + 3) {
self.pos = start + 3;
return Ok((TokKind::Float(f64::INFINITY), start, self.pos));
}
let first_len = self
.char_at(start)
.expect("caller already confirmed a char at `start`")
.len_utf8();
let mut p = start + first_len;
while self
.byte_at(p)
.is_some_and(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
{
p += 1;
}
let text: String = self.text[start..p].to_string();
self.pos = p;
Ok((TokKind::Ident(text), start, p))
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) enum TemporalKind {
Date,
Time,
Datetime,
}