Skip to main content

lux/
diagnostic.rs

1//! Source positions and error reporting.
2//!
3//! A `Span` is a byte range into the source. Every token and every ast node
4//! carries one, so when something goes wrong we can underline the offending
5//! text. `report` renders an error the way a teaching language should: the
6//! line, a caret under the problem, and an optional note suggesting the fix.
7
8/// A half-open byte range `[start, end)` into the source text.
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct Span {
11    pub start: usize,
12    pub end: usize,
13}
14
15impl Span {
16    pub fn new(start: usize, end: usize) -> Self {
17        Span { start, end }
18    }
19
20    /// A span stretching from the start of `self` to the end of `other`.
21    pub fn to(self, other: Span) -> Span {
22        Span::new(self.start, other.end)
23    }
24}
25
26/// An error with a message, the span it happened at, an optional note, and an
27/// optional trail to a `lux learn` topic — a `(topic, lure)` pair, where the
28/// lure is a one-line hint at why the idea is worth following.
29#[derive(Debug, Clone)]
30pub struct LuxError {
31    pub message: String,
32    pub span: Span,
33    pub note: Option<String>,
34    pub learn: Option<(&'static str, &'static str)>,
35}
36
37impl LuxError {
38    pub fn new(message: impl Into<String>, span: Span) -> Self {
39        LuxError {
40            message: message.into(),
41            span,
42            note: None,
43            learn: None,
44        }
45    }
46
47    /// Attach a hint about how to fix the problem.
48    pub fn with_note(mut self, note: impl Into<String>) -> Self {
49        self.note = Some(note.into());
50        self
51    }
52
53    /// Open a trail to the `lux learn` topic that teaches this, with a one-line
54    /// lure hinting at why it is worth following. Set it only where there is a
55    /// clean topic that genuinely covers the mistake; a self-evident fix needs
56    /// only a note, not a trail.
57    pub fn with_learn(mut self, topic: &'static str, lure: &'static str) -> Self {
58        self.learn = Some((topic, lure));
59        self
60    }
61}
62
63/// Print an error to stderr with the source line and a caret under the span.
64pub fn report(filename: &str, source: &str, err: &LuxError) {
65    let start = err.span.start.min(source.len());
66    let line_start = source[..start].rfind('\n').map(|i| i + 1).unwrap_or(0);
67    let line_end = source[start..]
68        .find('\n')
69        .map(|i| start + i)
70        .unwrap_or(source.len());
71    let line_no = source[..line_start].bytes().filter(|&b| b == b'\n').count() + 1;
72    let col = start - line_start + 1;
73    let line_text = &source[line_start..line_end];
74
75    let caret_end = err.span.end.min(line_end);
76    let caret_len = caret_end.saturating_sub(start).max(1);
77
78    eprintln!("error: {}", err.message);
79    eprintln!("  --> {}:{}:{}", filename, line_no, col);
80
81    let gutter = format!(" {} | ", line_no);
82    let blank: String = gutter.chars().map(|_| ' ').collect();
83    eprintln!("{}{}", gutter, line_text);
84    eprintln!("{}{}{}", blank, " ".repeat(col - 1), "^".repeat(caret_len));
85
86    if let Some(note) = &err.note {
87        eprintln!("note: {}", note);
88    }
89    if let Some((topic, lure)) = &err.learn {
90        eprintln!("help: `lux learn {}` — {}", topic, lure);
91    }
92}