Skip to main content

mimium_lang/utils/
error.rs

1use std::{
2    collections::HashMap,
3    path::PathBuf,
4    sync::{LazyLock, Mutex},
5};
6
7use ariadne::{ColorGenerator, Label, Report, ReportKind, Source};
8use thiserror::Error;
9
10use super::fileloader;
11use super::metadata::Location;
12
13/// A dynamic error type that can hold specific error messages and the location where the error happened.
14pub trait ReportableError: std::error::Error {
15    /// message is used for reporting verbose message for `ariadne``.
16    fn get_message(&self) -> String {
17        self.to_string()
18    }
19    /// Label is used for indicating error with the specific position for `ariadne``.
20    /// One error may have multiple labels, because the reason of the error may be caused by the mismatch of the properties in 2 or more different locations in the source (such as the type mismatch).
21    fn get_labels(&self) -> Vec<(Location, String)>;
22}
23
24/// ReportableError implements `PartialEq`` mostly for testing purpose.
25impl PartialEq for dyn ReportableError + '_ {
26    fn eq(&self, other: &Self) -> bool {
27        self.get_labels() == other.get_labels()
28    }
29}
30
31#[derive(Debug, Clone, Error)]
32#[error("{message}")]
33pub struct SimpleError {
34    pub message: String,
35    pub span: Location,
36}
37
38impl ReportableError for SimpleError {
39    fn get_labels(&self) -> Vec<(Location, String)> {
40        vec![(self.span.clone(), self.message.clone())]
41    }
42}
43
44#[derive(Debug, Clone, Error)]
45#[error("{message}")]
46pub struct RichError {
47    pub message: String,
48    pub labels: Vec<(Location, String)>,
49}
50
51impl ReportableError for RichError {
52    fn get_message(&self) -> String {
53        self.message.clone()
54    }
55    fn get_labels(&self) -> Vec<(Location, String)> {
56        self.labels.clone()
57    }
58}
59impl From<Box<dyn ReportableError + '_>> for RichError {
60    fn from(e: Box<dyn ReportableError + '_>) -> Self {
61        Self {
62            message: e.get_message(),
63            labels: e.get_labels(),
64        }
65    }
66}
67
68struct FileCache {
69    pub storage: HashMap<PathBuf, ariadne::Source<String>>,
70}
71
72impl ariadne::Cache<PathBuf> for FileCache {
73    type Storage = String;
74
75    fn fetch(&mut self, id: &PathBuf) -> Result<&Source<Self::Storage>, impl std::fmt::Debug> {
76        if !self.storage.contains_key(id)
77            && let Ok(content) = fileloader::load(id.to_string_lossy().as_ref())
78        {
79            self.storage.insert(id.clone(), Source::from(content));
80        }
81
82        self.storage
83            .get(id)
84            .ok_or_else(|| format!("File not found: {}", id.display()))
85    }
86
87    fn display<'a>(&self, id: &'a PathBuf) -> Option<impl std::fmt::Display + 'a> {
88        Some(id.display())
89    }
90}
91
92static FILE_BUCKET: LazyLock<Mutex<FileCache>> = LazyLock::new(|| {
93    Mutex::new(FileCache {
94        storage: HashMap::new(),
95    })
96});
97
98pub fn report(src: &str, path: PathBuf, errs: &[Box<dyn ReportableError + '_>]) {
99    let mut colors = ColorGenerator::new();
100    for e in errs {
101        // let a_span = (src.source(), span);color
102        let rawlabels = e
103            .get_labels()
104            .into_iter()
105            .map(|(mut loc, message)| {
106                if loc.path.as_os_str().is_empty() {
107                    loc.path = path.clone();
108                }
109                (loc, message)
110            })
111            .collect::<Vec<_>>();
112        if rawlabels.is_empty() {
113            continue;
114        }
115        let labels = rawlabels.iter().map(|(loc, message)| {
116            let span = (loc.path.clone(), loc.span.clone());
117            Label::new(span)
118                .with_message(message)
119                .with_color(colors.next())
120        });
121        let span = (rawlabels[0].0.path.clone(), rawlabels[0].0.span.clone());
122        let builder = Report::build(ReportKind::Error, span)
123            .with_message(e.get_message())
124            .with_labels(labels)
125            .finish();
126        if let Ok(mut cache) = FILE_BUCKET.lock() {
127            let mut cache: &mut FileCache = &mut cache;
128            cache
129                .storage
130                .insert(path.clone(), Source::from(src.to_string()));
131            if let Err(err) = builder.eprint(&mut cache) {
132                eprintln!("Error: {}", e.get_message());
133                eprintln!("  (rich diagnostic rendering failed: {err:?})");
134                for (loc, label) in &rawlabels {
135                    eprintln!(
136                        "  -> {}:{}..{}: {}",
137                        loc.path.display(),
138                        loc.span.start,
139                        loc.span.end,
140                        label
141                    );
142                }
143            }
144        }
145    }
146}
147
148pub fn dump_to_string(errs: &[Box<dyn ReportableError>]) -> String {
149    let mut res = String::new();
150    for e in errs {
151        res += e.get_message().as_str();
152    }
153    res
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159    use crate::compiler::parser::{parse_program, parser_errors_to_reportable};
160    use std::fs;
161
162    #[test]
163    fn report_handles_labels_in_other_files() {
164        let base_dir = std::env::current_dir().unwrap().join("tmp");
165        fs::create_dir_all(&base_dir).unwrap();
166
167        let root_path = base_dir.join("report_root.mmm");
168        let included_path = base_dir.join("report_included.mmm");
169
170        let root_src = "include(\"./report_included.mmm\")\n";
171        let included_src = "fn bad( {\n";
172        let expected_start = included_src.find('{').unwrap();
173
174        fs::write(&root_path, root_src).unwrap();
175        fs::write(&included_path, included_src).unwrap();
176
177        let canonical_included_path = fs::canonicalize(&included_path).unwrap();
178        let (_program, parse_errs) = parse_program(included_src, canonical_included_path.clone());
179        assert!(!parse_errs.is_empty());
180
181        let errs =
182            parser_errors_to_reportable(included_src, canonical_included_path.clone(), parse_errs);
183
184        assert!(errs[0].get_message().starts_with("Parse error:"));
185        let labels = errs[0].get_labels();
186        assert_eq!(labels.len(), 1);
187        assert!(labels[0].1.starts_with("Parse error:"));
188        assert_eq!(labels[0].0.span, expected_start..expected_start + 1);
189        assert_eq!(labels[0].0.path, canonical_included_path);
190
191        report(root_src, root_path, &errs);
192    }
193
194    #[test]
195    fn report_falls_back_when_label_path_is_empty() {
196        let path = std::env::current_dir()
197            .unwrap()
198            .join("tmp")
199            .join("report_empty_path.mmm");
200        fs::create_dir_all(path.parent().unwrap()).unwrap();
201        let src = "fn dsp(){0.0}\n";
202        fs::write(&path, src).unwrap();
203
204        let err = Box::new(SimpleError {
205            message: "dummy".to_string(),
206            span: Location::default(),
207        }) as Box<dyn ReportableError>;
208
209        report(src, path, &[err]);
210    }
211}