use core::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Span {
pub offset: usize,
pub len: usize,
pub line: u32,
pub col: u32,
}
impl Span {
#[must_use]
pub fn from_offset(source: &str, offset: usize, len: usize) -> Self {
let clamped = offset.min(source.len());
let prefix = &source.as_bytes()[..clamped];
let mut line: u32 = 1;
let mut last_newline: usize = 0;
for (i, &b) in prefix.iter().enumerate() {
if b == b'\n' {
line = line.saturating_add(1);
last_newline = i + 1;
}
}
#[allow(clippy::cast_possible_truncation)]
let col = (clamped - last_newline) as u32 + 1;
Self {
offset,
len,
line,
col,
}
}
}
impl fmt::Display for Span {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.line, self.col)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseError {
pub message: String,
pub span: Span,
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Parse error at {}: {}", self.span, self.message)
}
}
impl std::error::Error for ParseError {}
#[doc(hidden)]
#[cold]
#[track_caller]
#[inline(never)]
#[must_use]
pub fn cold_parse_error_unexpected_eof(span: Span) -> ParseError {
ParseError {
message: "Unexpected end of input; expected closing ')'".to_owned(),
span,
}
}
#[doc(hidden)]
#[cold]
#[track_caller]
#[inline(never)]
#[must_use]
pub fn cold_parse_error_unexpected_token(span: Span, bad: char) -> ParseError {
ParseError {
message: format!("Unexpected character {bad:?}; expected a number, variable, or '('"),
span,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn span_at_start_of_input() {
let s = Span::from_offset("hello", 0, 1);
assert_eq!(s.line, 1);
assert_eq!(s.col, 1);
}
#[test]
fn span_after_newline() {
let src = "a + b\nc + d\nbad";
let off = src.find("bad").expect("find");
let s = Span::from_offset(src, off, 3);
assert_eq!(s.line, 3);
assert_eq!(s.col, 1);
}
#[test]
fn span_offset_past_end_clamps() {
let s = Span::from_offset("xy", 999, 1);
assert_eq!(s.line, 1);
assert_eq!(s.col, 3); }
}