use core::fmt;
use crate::grammar::TokenKind;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Span {
pub start: usize,
pub end: usize,
}
impl Span {
pub const SYNTHETIC: Self = Self { start: 0, end: 0 };
#[must_use]
pub const fn new(start: usize, end: usize) -> Self {
Self { start, end }
}
#[must_use]
pub const fn point(at: usize) -> Self {
Self { start: at, end: at }
}
#[must_use]
pub const fn len(&self) -> usize {
self.end.saturating_sub(self.start)
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.start == self.end
}
#[must_use]
pub const fn merge(self, other: Self) -> Self {
Self {
start: if self.start < other.start {
self.start
} else {
other.start
},
end: if self.end > other.end {
self.end
} else {
other.end
},
}
}
#[must_use]
pub const fn contains_offset(&self, byte_offset: usize) -> bool {
byte_offset >= self.start && byte_offset < self.end
}
}
impl fmt::Display for Span {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}..{}", self.start, self.end)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Severity {
Help,
Note,
Warning,
Error,
}
impl fmt::Display for Severity {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let label = match self {
Self::Help => "help",
Self::Note => "note",
Self::Warning => "warning",
Self::Error => "error",
};
f.write_str(label)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Label {
pub span: Span,
pub message: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub severity: Severity,
pub message: String,
pub primary_span: Span,
pub labels: Vec<Label>,
}
impl Diagnostic {
#[must_use]
pub fn error(message: impl Into<String>, primary_span: Span) -> Self {
Self {
severity: Severity::Error,
message: message.into(),
primary_span,
labels: Vec::new(),
}
}
#[must_use]
pub fn warning(message: impl Into<String>, primary_span: Span) -> Self {
Self {
severity: Severity::Warning,
message: message.into(),
primary_span,
labels: Vec::new(),
}
}
#[must_use]
pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
self.labels.push(Label {
span,
message: message.into(),
});
self
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParseError {
UnexpectedToken {
found: TokenKind,
expected: Vec<TokenKind>,
span: Span,
},
UnexpectedEof {
expected: Vec<TokenKind>,
span: Span,
},
LexerError {
message: String,
span: Span,
},
}
impl ParseError {
#[must_use]
pub fn to_diagnostic(&self) -> Diagnostic {
match self {
Self::UnexpectedToken {
found,
expected,
span,
} => {
let msg = format!(
"unexpected token {:?}, expected {}",
found,
format_expected(expected),
);
Diagnostic::error(msg, *span)
}
Self::UnexpectedEof { expected, span } => {
let msg = format!(
"unexpected end of input, expected {}",
format_expected(expected),
);
Diagnostic::error(msg, *span)
}
Self::LexerError { message, span } => Diagnostic::error(message.clone(), *span),
}
}
#[must_use]
pub const fn span(&self) -> Span {
match self {
Self::UnexpectedToken { span, .. }
| Self::UnexpectedEof { span, .. }
| Self::LexerError { span, .. } => *span,
}
}
}
fn format_expected(expected: &[TokenKind]) -> String {
match expected {
[] => "(nothing)".to_string(),
[single] => format!("{single:?}"),
many => {
let formatted: Vec<String> = many.iter().map(|t| format!("{t:?}")).collect();
format!("one of [{}]", formatted.join(", "))
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn span_new_stores_start_and_end() {
let s = Span::new(3, 7);
assert_eq!(s.start, 3);
assert_eq!(s.end, 7);
}
#[test]
fn span_point_has_zero_length() {
let s = Span::point(42);
assert_eq!(s.start, 42);
assert_eq!(s.end, 42);
assert!(s.is_empty());
assert_eq!(s.len(), 0);
}
#[test]
fn span_len_counts_bytes() {
assert_eq!(Span::new(0, 5).len(), 5);
assert_eq!(Span::new(10, 14).len(), 4);
}
#[test]
fn span_merge_takes_outer_bounds() {
let a = Span::new(2, 5);
let b = Span::new(4, 8);
assert_eq!(a.merge(b), Span::new(2, 8));
assert_eq!(b.merge(a), Span::new(2, 8));
}
#[test]
fn span_merge_with_synthetic_keeps_real_bounds() {
let real = Span::new(10, 20);
let merged = real.merge(Span::SYNTHETIC);
assert_eq!(merged, Span::new(0, 20));
}
#[test]
fn span_contains_offset_is_half_open() {
let s = Span::new(5, 10);
assert!(s.contains_offset(5));
assert!(s.contains_offset(7));
assert!(s.contains_offset(9));
assert!(!s.contains_offset(10), "end ist exklusiv");
assert!(!s.contains_offset(4));
}
#[test]
fn span_displays_as_start_dot_dot_end() {
assert_eq!(format!("{}", Span::new(3, 7)), "3..7");
}
#[test]
fn severity_orders_help_lower_than_error() {
assert!(Severity::Help < Severity::Note);
assert!(Severity::Note < Severity::Warning);
assert!(Severity::Warning < Severity::Error);
}
#[test]
fn severity_displays_lowercase_label() {
assert_eq!(format!("{}", Severity::Error), "error");
assert_eq!(format!("{}", Severity::Warning), "warning");
assert_eq!(format!("{}", Severity::Note), "note");
assert_eq!(format!("{}", Severity::Help), "help");
}
#[test]
fn diagnostic_error_constructor_sets_severity() {
let d = Diagnostic::error("oops", Span::new(0, 3));
assert_eq!(d.severity, Severity::Error);
assert_eq!(d.message, "oops");
assert_eq!(d.primary_span, Span::new(0, 3));
assert!(d.labels.is_empty());
}
#[test]
fn diagnostic_warning_constructor_sets_severity() {
let d = Diagnostic::warning("hmm", Span::new(2, 4));
assert_eq!(d.severity, Severity::Warning);
}
#[test]
fn diagnostic_with_label_appends_in_order() {
let d = Diagnostic::error("bad", Span::new(0, 3))
.with_label(Span::new(5, 10), "see here")
.with_label(Span::new(20, 22), "and here");
assert_eq!(d.labels.len(), 2);
assert_eq!(d.labels[0].message, "see here");
assert_eq!(d.labels[0].span, Span::new(5, 10));
assert_eq!(d.labels[1].message, "and here");
}
#[test]
fn parse_error_unexpected_token_to_diagnostic() {
let err = ParseError::UnexpectedToken {
found: TokenKind::Keyword("struct"),
expected: vec![TokenKind::Punct("{")],
span: Span::new(7, 13),
};
let d = err.to_diagnostic();
assert_eq!(d.severity, Severity::Error);
assert!(d.message.contains("unexpected token"));
assert!(d.message.contains("struct"));
assert!(d.message.contains("{"));
assert_eq!(d.primary_span, Span::new(7, 13));
}
#[test]
fn parse_error_unexpected_eof_to_diagnostic() {
let err = ParseError::UnexpectedEof {
expected: vec![TokenKind::Punct(";")],
span: Span::point(50),
};
let d = err.to_diagnostic();
assert!(d.message.contains("end of input"));
assert_eq!(d.primary_span, Span::point(50));
}
#[test]
fn parse_error_lexer_to_diagnostic_uses_message() {
let err = ParseError::LexerError {
message: "unterminated string literal".to_string(),
span: Span::new(10, 12),
};
let d = err.to_diagnostic();
assert_eq!(d.message, "unterminated string literal");
}
#[test]
fn parse_error_span_accessor_returns_inner_span() {
let err = ParseError::UnexpectedToken {
found: TokenKind::Ident,
expected: vec![],
span: Span::new(3, 6),
};
assert_eq!(err.span(), Span::new(3, 6));
}
#[test]
fn format_expected_handles_empty_single_and_many() {
assert_eq!(format_expected(&[]), "(nothing)");
assert_eq!(format_expected(&[TokenKind::Ident]), "Ident");
let many = format_expected(&[TokenKind::Punct(";"), TokenKind::Punct(",")]);
assert!(many.contains("one of"));
assert!(many.contains(';'));
assert!(many.contains(','));
}
}