use alloc::format;
use alloc::string::{String, ToString};
use crate::core::Error;
use crate::machine::span::{SourceFile, Span};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Note {
pub message: String,
pub span: Span,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Diagnostic {
pub message: String,
pub span: Span,
pub note: Option<Note>,
}
impl Diagnostic {
pub fn new(span: Span, message: impl Into<String>) -> Self {
Diagnostic {
message: message.into(),
span,
note: None,
}
}
#[must_use]
pub fn with_note(mut self, span: Span, message: impl Into<String>) -> Self {
self.note = Some(Note {
message: message.into(),
span,
});
self
}
pub fn render(&self, src: &SourceFile<'_>) -> String {
self.render_in(src)
}
pub fn render_in(&self, sources: &impl Sources) -> String {
let (src, span) = sources.locate(self.span);
let mut out = format!("error: {}\n", self.message);
out.push_str(&header(&src, span));
out.push_str(&snippet(&src, span));
if let Some(note) = &self.note {
let (nsrc, nspan) = sources.locate(note.span);
out.push_str(&format!("\nnote: {}\n", note.message));
out.push_str(&header(&nsrc, nspan));
out.push_str(&snippet(&nsrc, nspan));
}
while out.ends_with('\n') {
out.pop();
}
out
}
pub fn to_error(&self, src: &SourceFile<'_>) -> Error {
self.to_error_in(src)
}
pub fn to_error_in(&self, sources: &impl Sources) -> Error {
let (src, span) = sources.locate(self.span);
let mut message = self.message.clone();
message.push('\n');
message.push_str(&snippet(&src, span));
if let Some(note) = &self.note {
let (nsrc, nspan) = sources.locate(note.span);
message.push_str(&format!(
"note: {} (at {})\n",
note.message,
nsrc.position(nspan.start)
));
message.push_str(&snippet(&nsrc, nspan));
}
while message.ends_with('\n') {
message.pop();
}
Error::Config {
at: src.position(span.start),
message,
}
}
}
pub trait Sources {
fn locate(&self, span: Span) -> (SourceFile<'_>, Span);
}
impl Sources for SourceFile<'_> {
fn locate(&self, span: Span) -> (SourceFile<'_>, Span) {
(*self, span)
}
}
fn header(src: &SourceFile<'_>, span: Span) -> String {
let loc = src.location(span.start);
let width = digits(loc.line);
format!(
"{:width$}--> {}\n",
"",
src.position(span.start),
width = width
)
}
fn snippet(src: &SourceFile<'_>, span: Span) -> String {
let loc = src.location(span.start);
let line = src.line_text(loc.line);
let width = digits(loc.line);
let end = src.location(span.end);
let line_chars = line.chars().count();
let end_col = if end.line == loc.line {
end.col as usize
} else {
line_chars + 1
};
let carets = end_col.saturating_sub(loc.col as usize).max(1);
let mut pad = String::new();
for c in line.chars().take(loc.col.saturating_sub(1) as usize) {
pad.push(if c == '\t' { '\t' } else { ' ' });
}
let mut out = format!("{:width$} |\n", "", width = width);
if line.is_empty() {
out.push_str(&format!(
"{:>width$} |\n",
loc.line.to_string(),
width = width
));
} else {
out.push_str(&format!(
"{:>width$} | {}\n",
loc.line.to_string(),
line,
width = width
));
}
out.push_str(&format!("{:width$} | {}", "", pad, width = width));
for _ in 0..carets {
out.push('^');
}
out.push('\n');
out
}
fn digits(mut n: u32) -> usize {
let mut d = 1;
while n >= 10 {
n /= 10;
d += 1;
}
d
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
#[test]
fn renders_message_location_and_caret() {
let text = "machine \"nes\" {\n param x = 1\n}\n";
let src = SourceFile::new("nes.machine", text);
let d = Diagnostic::new(Span::new(18, 23), "unknown statement `param`");
assert_eq!(
d.render(&src),
"\
error: unknown statement `param`\n \
--> nes.machine:2:3\n \
|\n\
2 | param x = 1\n \
| ^^^^^"
);
}
#[test]
fn a_note_adds_a_second_block() {
let text = "a {\nb\n";
let src = SourceFile::new("m", text);
let d = Diagnostic::new(Span::at(6), "expected `}`, found end of file")
.with_note(Span::new(2, 3), "unclosed `{` here");
assert_eq!(
d.render(&src),
"\
error: expected `}`, found end of file\n \
--> m:3:1\n \
|\n\
3 |\n \
| ^\n\
\n\
note: unclosed `{` here\n \
--> m:1:3\n \
|\n\
1 | a {\n \
| ^"
);
}
#[test]
fn to_error_puts_the_location_in_at_and_the_caret_in_message() {
let src = SourceFile::new("m", "x = 1\n");
let d = Diagnostic::new(Span::new(4, 5), "bad value");
let err = d.to_error(&src);
match &err {
Error::Config { at, message } => {
assert_eq!(at, "m:1:5");
assert_eq!(message, "bad value\n |\n1 | x = 1\n | ^");
}
other => panic!("wrong variant: {other:?}"),
}
assert_eq!(
err.to_string(),
"m:1:5: bad value\n |\n1 | x = 1\n | ^"
);
}
#[test]
fn the_gutter_widens_with_the_line_number() {
let mut text = String::new();
for _ in 0..11 {
text.push_str("x\n");
}
text.push_str("bad\n");
let src = SourceFile::new("m", &text);
let d = Diagnostic::new(Span::new(22, 25), "here");
assert_eq!(
d.render(&src),
"error: here\n --> m:12:1\n |\n12 | bad\n | ^^^"
);
}
#[test]
fn a_multi_line_span_carets_only_its_first_line() {
let src = SourceFile::new("m", "abc\ndef\n");
let d = Diagnostic::new(Span::new(1, 6), "spans two lines");
assert!(d.render(&src).ends_with("1 | abc\n | ^^"));
}
#[test]
fn tabs_in_the_indent_are_preserved_in_the_caret_padding() {
let src = SourceFile::new("m", "\tx = 1\n");
let d = Diagnostic::new(Span::new(1, 2), "here");
assert!(d.render(&src).ends_with("1 | \tx = 1\n | \t^"));
}
#[test]
fn digits_counts_decimal_places() {
assert_eq!(digits(0), 1);
assert_eq!(digits(9), 1);
assert_eq!(digits(10), 2);
assert_eq!(digits(u32::MAX), 10);
}
}