use std::{
collections::HashMap,
path::PathBuf,
sync::{LazyLock, Mutex},
};
use ariadne::{ColorGenerator, Label, Report, ReportKind, Source};
use thiserror::Error;
use super::fileloader;
use super::metadata::Location;
pub trait ReportableError: std::error::Error {
fn get_message(&self) -> String {
self.to_string()
}
fn get_labels(&self) -> Vec<(Location, String)>;
}
impl PartialEq for dyn ReportableError + '_ {
fn eq(&self, other: &Self) -> bool {
self.get_labels() == other.get_labels()
}
}
#[derive(Debug, Clone, Error)]
#[error("{message}")]
pub struct SimpleError {
pub message: String,
pub span: Location,
}
impl ReportableError for SimpleError {
fn get_labels(&self) -> Vec<(Location, String)> {
vec![(self.span.clone(), self.message.clone())]
}
}
#[derive(Debug, Clone, Error)]
#[error("{message}")]
pub struct RichError {
pub message: String,
pub labels: Vec<(Location, String)>,
}
impl ReportableError for RichError {
fn get_message(&self) -> String {
self.message.clone()
}
fn get_labels(&self) -> Vec<(Location, String)> {
self.labels.clone()
}
}
impl From<Box<dyn ReportableError + '_>> for RichError {
fn from(e: Box<dyn ReportableError + '_>) -> Self {
Self {
message: e.get_message(),
labels: e.get_labels(),
}
}
}
struct FileCache {
pub storage: HashMap<PathBuf, ariadne::Source<String>>,
}
impl ariadne::Cache<PathBuf> for FileCache {
type Storage = String;
fn fetch(&mut self, id: &PathBuf) -> Result<&Source<Self::Storage>, impl std::fmt::Debug> {
if !self.storage.contains_key(id)
&& let Ok(content) = fileloader::load(id.to_string_lossy().as_ref())
{
self.storage.insert(id.clone(), Source::from(content));
}
self.storage
.get(id)
.ok_or_else(|| format!("File not found: {}", id.display()))
}
fn display<'a>(&self, id: &'a PathBuf) -> Option<impl std::fmt::Display + 'a> {
Some(id.display())
}
}
static FILE_BUCKET: LazyLock<Mutex<FileCache>> = LazyLock::new(|| {
Mutex::new(FileCache {
storage: HashMap::new(),
})
});
pub fn report(src: &str, path: PathBuf, errs: &[Box<dyn ReportableError + '_>]) {
let mut colors = ColorGenerator::new();
for e in errs {
let rawlabels = e
.get_labels()
.into_iter()
.map(|(mut loc, message)| {
if loc.path.as_os_str().is_empty() {
loc.path = path.clone();
}
(loc, message)
})
.collect::<Vec<_>>();
if rawlabels.is_empty() {
continue;
}
let labels = rawlabels.iter().map(|(loc, message)| {
let span = (loc.path.clone(), loc.span.clone());
Label::new(span)
.with_message(message)
.with_color(colors.next())
});
let span = (rawlabels[0].0.path.clone(), rawlabels[0].0.span.clone());
let builder = Report::build(ReportKind::Error, span)
.with_message(e.get_message())
.with_labels(labels)
.finish();
if let Ok(mut cache) = FILE_BUCKET.lock() {
let mut cache: &mut FileCache = &mut cache;
cache
.storage
.insert(path.clone(), Source::from(src.to_string()));
if let Err(err) = builder.eprint(&mut cache) {
eprintln!("Error: {}", e.get_message());
eprintln!(" (rich diagnostic rendering failed: {err:?})");
for (loc, label) in &rawlabels {
eprintln!(
" -> {}:{}..{}: {}",
loc.path.display(),
loc.span.start,
loc.span.end,
label
);
}
}
}
}
}
pub fn dump_to_string(errs: &[Box<dyn ReportableError>]) -> String {
let mut res = String::new();
for e in errs {
res += e.get_message().as_str();
}
res
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compiler::parser::{parse_program, parser_errors_to_reportable};
use std::fs;
#[test]
fn report_handles_labels_in_other_files() {
let base_dir = std::env::current_dir().unwrap().join("tmp");
fs::create_dir_all(&base_dir).unwrap();
let root_path = base_dir.join("report_root.mmm");
let included_path = base_dir.join("report_included.mmm");
let root_src = "include(\"./report_included.mmm\")\n";
let included_src = "fn bad( {\n";
let expected_start = included_src.find('{').unwrap();
fs::write(&root_path, root_src).unwrap();
fs::write(&included_path, included_src).unwrap();
let canonical_included_path = fs::canonicalize(&included_path).unwrap();
let (_program, parse_errs) = parse_program(included_src, canonical_included_path.clone());
assert!(!parse_errs.is_empty());
let errs =
parser_errors_to_reportable(included_src, canonical_included_path.clone(), parse_errs);
assert!(errs[0].get_message().starts_with("Parse error:"));
let labels = errs[0].get_labels();
assert_eq!(labels.len(), 1);
assert!(labels[0].1.starts_with("Parse error:"));
assert_eq!(labels[0].0.span, expected_start..expected_start + 1);
assert_eq!(labels[0].0.path, canonical_included_path);
report(root_src, root_path, &errs);
}
#[test]
fn report_falls_back_when_label_path_is_empty() {
let path = std::env::current_dir()
.unwrap()
.join("tmp")
.join("report_empty_path.mmm");
fs::create_dir_all(path.parent().unwrap()).unwrap();
let src = "fn dsp(){0.0}\n";
fs::write(&path, src).unwrap();
let err = Box::new(SimpleError {
message: "dummy".to_string(),
span: Location::default(),
}) as Box<dyn ReportableError>;
report(src, path, &[err]);
}
}