mimium_lang/utils/
error.rs1use 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
13pub trait ReportableError: std::error::Error {
15 fn get_message(&self) -> String {
17 self.to_string()
18 }
19 fn get_labels(&self) -> Vec<(Location, String)>;
22}
23
24impl 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 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}