diagnostics.rs
raw
//! 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<String>, 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)
}