Skip to main content

lutra_compiler/
error.rs

1use std::collections::HashMap;
2use std::fmt::{self, Write};
3use std::path::{Path, PathBuf};
4
5use itertools::Itertools;
6
7use crate::codespan;
8use crate::diagnostic::{Diagnostic, DiagnosticCode};
9
10#[derive(thiserror::Error, Debug)]
11pub enum Error {
12    #[error("io error: {0}")]
13    Io(#[from] std::io::Error),
14
15    #[error("invalid path: {path}")]
16    InvalidPath { path: PathBuf },
17
18    #[error("cannot find project root")]
19    CannotFindProjectRoot,
20
21    #[error("cannot read source file at {file}:\n  {io}")]
22    CannotReadSourceFile {
23        file: std::path::PathBuf,
24        io: std::io::Error,
25    },
26
27    #[error("{}", DisplayMessages(.diagnostics))]
28    Compile { diagnostics: Vec<DiagnosticMessage> },
29}
30
31impl Error {
32    pub(crate) fn from_diagnostics(
33        diagnostics: Vec<Diagnostic>,
34        sources: &impl crate::project::SourceProvider,
35    ) -> Self {
36        let diagnostics = compose_diagnostic_messages(diagnostics, sources);
37        Error::Compile { diagnostics }
38    }
39}
40
41#[derive(Debug)]
42pub struct DiagnosticMessage {
43    diagnostic: Diagnostic,
44    display: String,
45    range: codespan::Range,
46    additional_ranges: Vec<Option<codespan::Range>>,
47}
48
49impl DiagnosticMessage {
50    pub fn code(&self) -> &'static str {
51        self.diagnostic.code.get()
52    }
53
54    pub fn message(&self) -> &str {
55        &self.diagnostic.message
56    }
57
58    pub fn span(&self) -> &Option<crate::Span> {
59        &self.diagnostic.span
60    }
61
62    pub fn additional(&self) -> Vec<Additional<'_>> {
63        self.diagnostic
64            .additional
65            .iter()
66            .enumerate()
67            .map(|(i, additional)| Additional {
68                additional,
69                range: self.additional_ranges.get(i).and_then(|x| x.as_ref()),
70            })
71            .collect()
72    }
73
74    pub fn display(&self) -> &str {
75        &self.display
76    }
77
78    pub fn range(&self) -> &codespan::Range {
79        &self.range
80    }
81}
82
83#[derive(Debug)]
84pub struct Additional<'d> {
85    additional: &'d crate::diagnostic::Additional,
86    range: Option<&'d codespan::Range>,
87}
88
89impl<'d> Additional<'d> {
90    pub fn message(&self) -> &'d str {
91        &self.additional.message
92    }
93
94    pub fn span(&self) -> Option<codespan::Span> {
95        self.additional.span
96    }
97
98    pub fn range(&self) -> Option<&'d codespan::Range> {
99        self.range
100    }
101}
102
103fn compose_diagnostic_messages(
104    diagnostics: Vec<Diagnostic>,
105    sources: &impl crate::project::SourceProvider,
106) -> Vec<DiagnosticMessage> {
107    let mut cache = FileTreeCache::new(sources);
108
109    let mut messages = Vec::with_capacity(diagnostics.len());
110    for diagnostic in diagnostics {
111        let Some(span) = diagnostic.span else {
112            panic!(
113                "missing diagnostic span: [{:?}] {}, {:#?}",
114                diagnostic.code, diagnostic.message, diagnostic.additional
115            );
116        };
117
118        let range = compose_range(span, sources, &mut cache);
119
120        let display = compose_display(&diagnostic, sources, &mut cache);
121
122        let mut additional_ranges = Vec::with_capacity(diagnostic.additional.len());
123        for a in &diagnostic.additional {
124            let range = a.span.map(|s| compose_range(s, sources, &mut cache));
125            additional_ranges.push(range);
126        }
127
128        messages.push(DiagnosticMessage {
129            diagnostic,
130            display,
131            range,
132            additional_ranges,
133        });
134    }
135    messages
136}
137
138fn compose_display<S>(
139    diagnostic: &Diagnostic,
140    sources: &impl crate::project::SourceProvider,
141    cache: &mut FileTreeCache<S>,
142) -> String
143where
144    S: crate::project::SourceProvider,
145{
146    use ariadne::{Config, Label, Report, ReportKind};
147
148    let config = Config::default().with_color(false);
149
150    let span = diagnostic.span.unwrap();
151    let (source_path, _) = sources.get_by_id(span.source_id).unwrap();
152    let span = std::ops::Range::from(span);
153
154    let kind = match diagnostic.code.get_severity() {
155        crate::diagnostic::Severity::Warning => ReportKind::Warning,
156        crate::diagnostic::Severity::Error => ReportKind::Error,
157    };
158
159    let mut report = Report::build(kind, source_path, span.start)
160        .with_config(config)
161        .with_label(Label::new((source_path, span)).with_message(&diagnostic.message));
162
163    if diagnostic.code != DiagnosticCode::CUSTOM {
164        report = report.with_code(diagnostic.code.get());
165    }
166
167    let mut notes = String::new();
168    for additional in &diagnostic.additional {
169        if let Some(span) = additional.span {
170            let span = std::ops::Range::from(span);
171            report.add_label(Label::new((source_path, span)).with_message(&diagnostic.message))
172        } else {
173            notes += "\n   │ ";
174            notes += &additional.message;
175        }
176    }
177    if !notes.is_empty() {
178        report.set_note(notes);
179    }
180
181    let mut out = Vec::new();
182    report.finish().write(cache, &mut out).unwrap();
183    let out = String::from_utf8(out).unwrap();
184    out.lines().map(|l| l.trim_end()).join("\n")
185}
186
187fn compose_range<S>(
188    span: codespan::Span,
189    sources: &impl crate::project::SourceProvider,
190    cache: &mut FileTreeCache<S>,
191) -> codespan::Range
192where
193    S: crate::project::SourceProvider,
194{
195    use ariadne::Cache;
196
197    let (source_path, _content) = sources.get_by_id(span.source_id).unwrap();
198    let source = cache.fetch(&source_path).unwrap();
199    let source_len = source.len();
200
201    let Some(start) = source.get_byte_line(span.start as usize) else {
202        panic!("span {span:?} is out of bounds of the source (len = {source_len})",);
203    };
204    let start = codespan::LineColumn {
205        line: start.1 as u32,
206        column: start.2 as u32,
207    };
208
209    let Some(end) = source.get_byte_line(span.start as usize + span.len as usize) else {
210        panic!("span {span:?} is out of bounds of the source (len = {source_len})",);
211    };
212    let end = codespan::LineColumn {
213        line: end.1 as u32,
214        column: end.2 as u32,
215    };
216    codespan::Range { start, end }
217}
218
219struct FileTreeCache<'a, S: crate::project::SourceProvider> {
220    provider: &'a S,
221    cache: HashMap<PathBuf, ariadne::Source>,
222}
223impl<'a, S: crate::project::SourceProvider> FileTreeCache<'a, S> {
224    fn new(file_tree: &'a S) -> Self {
225        FileTreeCache {
226            provider: file_tree,
227            cache: HashMap::new(),
228        }
229    }
230}
231
232impl<'a, S: crate::project::SourceProvider> ariadne::Cache<&Path> for FileTreeCache<'a, S> {
233    type Storage = String;
234    fn fetch(&mut self, path: &&Path) -> Result<&ariadne::Source, Box<dyn fmt::Debug + '_>> {
235        let (_, content) = match self.provider.get_by_path(path) {
236            Some(v) => v,
237            None => return Err(Box::new(format!("Unknown file `{path:?}`"))),
238        };
239
240        Ok(self
241            .cache
242            .entry((*path).to_owned())
243            .or_insert_with(|| ariadne::Source::from(content.to_string())))
244    }
245
246    fn display<'b>(&self, id: &&'b Path) -> Option<Box<dyn fmt::Display + 'b>> {
247        if id.as_os_str().is_empty() {
248            Some(Box::new(
249                self.provider.get_root().file_name()?.display().to_string(),
250            ))
251        } else {
252            Some(Box::new(id.display()))
253        }
254    }
255}
256
257struct DisplayMessages<'a>(&'a Vec<DiagnosticMessage>);
258
259impl<'a> std::fmt::Display for DisplayMessages<'a> {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        for d in self.0 {
262            f.write_str(&d.display)?;
263            f.write_char('\n')?;
264        }
265        Ok(())
266    }
267}