Skip to main content

lang_forge/
error.rs

1//! [`Error`]: why a schematic could not be forged, or a pipeline assembled.
2
3use alloc::{boxed::Box, vec::Vec};
4use core::fmt;
5
6use diag_lang::{Diagnostic, Label, Severity};
7use syntax_lang::Span;
8
9/// Why a schematic could not be forged into a [`Language`](crate::Language),
10/// or a capability pipeline could not be assembled.
11///
12/// Forging checks the whole schematic and reports every problem it finds, not
13/// just the first, so one round of fixes is usually enough. Each problem is a
14/// [`Diagnostic`] whose label points into the schematic text: byte offsets
15/// counted from the start of the string handed to
16/// [`Language::from_lsf`](crate::Language::from_lsf). Add that text to a fresh
17/// [`SourceMap`](diag_lang::SourceMap) and the spans line up, so
18/// [`Renderer`](diag_lang::Renderer) can draw each problem under the line at
19/// fault.
20///
21/// `Display` prints the first problem as `line:column: message`, which is
22/// enough for a log line or a test failure; render the diagnostics for the
23/// full picture.
24///
25/// # What to do with one
26///
27/// An `Error` always means the schematic needs editing: forging is
28/// deterministic, so retrying the same text fails the same way. Every message
29/// names what was found and what was expected, and many carry a `help` line
30/// with the fix.
31///
32/// # Examples
33///
34/// ```
35/// use lang_forge::Language;
36/// use lang_forge::diag_lang::{Renderer, SourceMap};
37///
38/// let schematic = r#"
39/// [language]
40/// name = "broken"
41///
42/// [rules]
43/// program = "stmt*"
44/// stmt    = "exprr ';'"
45/// expr    = "NUMBER"
46/// "#;
47///
48/// let err = Language::from_lsf(schematic).unwrap_err();
49/// assert_eq!(err.to_string(), "7:12: undefined rule `exprr`");
50/// assert_eq!(err.diagnostics().len(), 1);
51///
52/// // Render it with source context.
53/// let mut map = SourceMap::new();
54/// map.add("broken.lsf", schematic).expect("fits");
55/// let text = Renderer::new().render(&err.diagnostics()[0], &map);
56/// assert!(text.contains("help: did you mean `expr`?"));
57/// ```
58#[derive(Clone, Debug, PartialEq, Eq)]
59pub struct Error {
60    diagnostics: Vec<Diagnostic>,
61    line: u32,
62    column: u32,
63}
64
65impl Error {
66    /// An error from collected diagnostics, locating the first in `text`.
67    pub(crate) fn new(diagnostics: Vec<Diagnostic>, text: &str) -> Self {
68        let offset = diagnostics
69            .first()
70            .map_or(0, |d| d.primary().span().start().to_usize());
71        let (line, column) = line_col(text, offset);
72        Self {
73            diagnostics,
74            line,
75            column,
76        }
77    }
78
79    /// An error whose first diagnostic is already located.
80    pub(crate) fn located(diagnostics: Vec<Diagnostic>, line: u32, column: u32) -> Self {
81        Self {
82            diagnostics,
83            line,
84            column,
85        }
86    }
87
88    /// Every problem found, in the order they appear in the schematic.
89    ///
90    /// Never empty. Spans are byte offsets into the schematic text.
91    ///
92    /// # Examples
93    ///
94    /// ```
95    /// use lang_forge::Language;
96    ///
97    /// let err = Language::from_lsf("[language]\n[rules]\n").unwrap_err();
98    /// let messages: Vec<&str> = err.diagnostics().iter().map(|d| d.message()).collect();
99    /// assert_eq!(messages, ["missing `name` in [language]", "[rules] declares no rules"]);
100    /// ```
101    #[inline]
102    #[must_use]
103    pub fn diagnostics(&self) -> &[Diagnostic] {
104        &self.diagnostics
105    }
106}
107
108impl fmt::Display for Error {
109    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
110        let message = self.diagnostics.first().map_or("", |d| d.message());
111        write!(f, "{}:{}: {}", self.line, self.column, message)?;
112        match self.diagnostics.len() {
113            0 | 1 => Ok(()),
114            2 => f.write_str(" (and 1 more error)"),
115            n => write!(f, " (and {} more errors)", n - 1),
116        }
117    }
118}
119
120impl core::error::Error for Error {}
121
122/// The 1-based line and column (in characters) of byte `offset` in `text`.
123pub(crate) fn line_col(text: &str, offset: usize) -> (u32, u32) {
124    let mut offset = offset.min(text.len());
125    while !text.is_char_boundary(offset) {
126        offset -= 1;
127    }
128    let before = &text[..offset];
129    let line_start = before.rfind('\n').map_or(0, |i| i + 1);
130    let line = before.bytes().filter(|b| *b == b'\n').count() + 1;
131    let column = before[line_start..].chars().count() + 1;
132    (saturate(line), saturate(column))
133}
134
135fn saturate(n: usize) -> u32 {
136    u32::try_from(n).unwrap_or(u32::MAX)
137}
138
139/// Collects the problems found while forging.
140#[derive(Debug, Default)]
141pub(crate) struct Report {
142    diagnostics: Vec<Diagnostic>,
143}
144
145impl Report {
146    /// Records an error at `span`.
147    pub(crate) fn error(&mut self, span: Span, message: impl Into<Box<str>>) {
148        self.diagnostics.push(Diagnostic::new(
149            Severity::Error,
150            message,
151            Label::unlabelled(span),
152        ));
153    }
154
155    /// Records an error at `span` with a `help` line.
156    pub(crate) fn error_help(
157        &mut self,
158        span: Span,
159        message: impl Into<Box<str>>,
160        help: impl Into<Box<str>>,
161    ) {
162        self.diagnostics.push(
163            Diagnostic::new(Severity::Error, message, Label::unlabelled(span)).with_help(help),
164        );
165    }
166
167    /// Records a fully built diagnostic.
168    pub(crate) fn diagnostic(&mut self, diagnostic: Diagnostic) {
169        self.diagnostics.push(diagnostic);
170    }
171
172    /// Whether nothing has been recorded.
173    pub(crate) fn is_clean(&self) -> bool {
174        self.diagnostics.is_empty()
175    }
176
177    /// Converts the report into an error, ordering problems by position.
178    pub(crate) fn into_error(mut self, text: &str) -> Error {
179        self.diagnostics
180            .sort_by_key(|d| d.primary().span().start().to_u32());
181        Error::new(self.diagnostics, text)
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use alloc::string::ToString;
188
189    use super::*;
190
191    #[test]
192    fn test_line_col_counts_characters_not_bytes() {
193        let text = "ab\nçd\n";
194        assert_eq!(line_col(text, 0), (1, 1));
195        assert_eq!(line_col(text, 3), (2, 1));
196        // `d` sits after the two-byte `ç`.
197        assert_eq!(line_col(text, 5), (2, 2));
198        assert_eq!(line_col(text, 999), (3, 1));
199    }
200
201    #[test]
202    fn test_line_col_inside_multibyte_char_does_not_panic() {
203        assert_eq!(line_col("ç", 1), (1, 1));
204    }
205
206    #[test]
207    fn test_display_counts_additional_errors() {
208        let mut report = Report::default();
209        report.error(Span::new(4, 5), "second");
210        report.error(Span::new(0, 1), "first");
211        let err = report.into_error("abcdef");
212        assert_eq!(err.to_string(), "1:1: first (and 1 more error)");
213
214        let mut report = Report::default();
215        for _ in 0..3 {
216            report.error(Span::new(0, 0), "same");
217        }
218        assert_eq!(
219            report.into_error("").to_string(),
220            "1:1: same (and 2 more errors)"
221        );
222    }
223
224    #[test]
225    fn test_report_help_is_attached() {
226        let mut report = Report::default();
227        report.error_help(Span::new(0, 1), "bad", "fix it");
228        assert!(!report.is_clean());
229        let err = report.into_error("x");
230        assert_eq!(err.diagnostics()[0].help().collect::<Vec<_>>(), ["fix it"]);
231    }
232}