//! Diagnostics with source excerpts. //! //! The compiler reports every error with the span it applies to; rendering //! turns spans into `file:line:col` plus a caret-underlined excerpt, in the //! style Rust programmers already read fluently. use std::fmt; use crate::sexpr::Span; #[derive(Debug, Clone, PartialEq)] pub struct Diagnostic { pub message: String, pub span: Span, } impl Diagnostic { pub fn new(message: impl Into, span: Span) -> Self { Self { message: message.into(), span, } } } impl fmt::Display for Diagnostic { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.message) } } /// Render diagnostics against their source, one block per diagnostic. pub fn render(diagnostics: &[Diagnostic], source: &str, filename: &str) -> String { let mut out = String::new(); for (i, diag) in diagnostics.iter().enumerate() { if i > 0 { out.push('\n'); } render_one(&mut out, diag, source, filename); } out } fn render_one(out: &mut String, diag: &Diagnostic, source: &str, filename: &str) { let (line_no, col, line_text, line_start) = locate(source, diag.span.start); let gutter = line_no.to_string(); let pad = " ".repeat(gutter.len()); out.push_str(&format!("error: {}\n", diag.message)); out.push_str(&format!(" --> {filename}:{line_no}:{col}\n")); out.push_str(&format!("{pad} |\n")); out.push_str(&format!("{gutter} | {line_text}\n")); // Caret run covering the span's portion of this line. let caret_start = diag.span.start - line_start; let caret_end = (diag.span.end - line_start) .min(line_text.len()) .max(caret_start + 1); let mut carets = String::new(); for (idx, _) in line_text.char_indices() { if idx < caret_start { carets.push(' '); } else if idx < caret_end { carets.push('^'); } } if carets.trim().is_empty() { carets = format!("{}^", " ".repeat(caret_start.min(line_text.len()))); } out.push_str(&format!("{pad} | {carets}\n")); } /// 1-based line, 1-based column, the line's text, and the line's byte start. fn locate(source: &str, offset: usize) -> (usize, usize, &str, usize) { let offset = offset.min(source.len()); let line_start = source[..offset].rfind('\n').map(|i| i + 1).unwrap_or(0); let line_no = source[..line_start].matches('\n').count() + 1; let line_end = source[line_start..] .find('\n') .map(|i| line_start + i) .unwrap_or(source.len()); let col = source[line_start..offset].chars().count() + 1; (line_no, col, &source[line_start..line_end], line_start) }