Skip to main content

jaq_all/
load.rs

1//! Pretty-printing compilation errors.
2use core::fmt::{self, Display, Formatter};
3use jaq_core::{compile, load};
4
5/// File and corresponding error reports.
6pub type FileReports<P = ()> = (load::File<String, P>, Vec<Report>);
7
8/// Report errors that may occur when loading a module.
9pub fn load_errors<P>(errs: load::Errors<&str, P>) -> Vec<FileReports<P>> {
10    use load::Error;
11
12    let errs = errs.into_iter().map(|(file, err)| {
13        let code = file.code;
14        let err = match err {
15            Error::Io(errs) => errs.into_iter().map(|e| report_io(code, e)).collect(),
16            Error::Lex(errs) => errs.into_iter().map(|e| report_lex(code, e)).collect(),
17            Error::Parse(errs) => errs.into_iter().map(|e| report_parse(code, e)).collect(),
18        };
19        (file.map_code(|s| s.into()), err)
20    });
21    errs.collect()
22}
23
24/// Report errors that may occur when compiling a module.
25pub fn compile_errors<P>(errs: compile::Errors<&str, P>) -> Vec<FileReports<P>> {
26    let errs = errs.into_iter().map(|(file, errs)| {
27        let code = file.code;
28        let errs = errs.into_iter().map(|e| report_compile(code, e)).collect();
29        (file.map_code(|s| s.into()), errs)
30    });
31    errs.collect()
32}
33
34type StringColors = Vec<(String, Option<Color>)>;
35
36/// Error report.
37#[derive(Debug)]
38pub struct Report {
39    /// error summary
40    message: String,
41    labels: Vec<(core::ops::Range<usize>, StringColors, Color)>,
42}
43
44/// Error color.
45#[derive(Copy, Clone, Debug)]
46pub enum Color {
47    /// used for most errors
48    Red = 31,
49    /// used for unclosed delimiters
50    Yellow = 33,
51}
52
53impl Color {
54    /// Format a string with ANSI colors.
55    pub fn ansi(self, f: &mut Formatter, text: &dyn Display) -> fmt::Result {
56        write!(f, "\x1b[{}m{}", self as usize, text)?;
57        write!(f, "\x1b[{}m", 0)
58    }
59}
60
61fn report_io(code: &str, (path, error): (&str, String)) -> Report {
62    let path_range = load::span(code, path);
63    Report {
64        message: format!("could not load file {path}: {error}"),
65        labels: [(path_range, [(error, None)].into(), Color::Red)].into(),
66    }
67}
68
69fn report_lex(code: &str, (expected, found): load::lex::Error<&str>) -> Report {
70    // truncate found string to its first character
71    let found = &found[..found.char_indices().nth(1).map_or(found.len(), |(i, _)| i)];
72
73    let found_range = load::span(code, found);
74    let found = match found {
75        "" => [("unexpected end of input".to_string(), None)].into(),
76        c => [("unexpected character ", None), (c, Some(Color::Red))]
77            .map(|(s, c)| (s.into(), c))
78            .into(),
79    };
80    let label = (found_range, found, Color::Red);
81
82    let labels = match expected {
83        load::lex::Expect::Delim(open) => {
84            let text = [("unclosed delimiter ", None), (open, Some(Color::Yellow))]
85                .map(|(s, c)| (s.into(), c));
86            Vec::from([(load::span(code, open), text.into(), Color::Yellow), label])
87        }
88        _ => Vec::from([label]),
89    };
90
91    Report {
92        message: format!("expected {}", expected.as_str()),
93        labels,
94    }
95}
96
97fn report_parse(code: &str, (expected, found): load::parse::Error<&str>) -> Report {
98    let found_range = load::span(code, found);
99
100    let found = if found.is_empty() {
101        "unexpected end of input"
102    } else {
103        "unexpected token"
104    };
105    let found = [(found.to_string(), None)].into();
106
107    Report {
108        message: format!("expected {}", expected.as_str()),
109        labels: Vec::from([(found_range, found, Color::Red)]),
110    }
111}
112
113fn report_compile(code: &str, (found, undefined): compile::Error<&str>) -> Report {
114    use compile::Undefined::Filter;
115    let found_range = load::span(code, found);
116    let wnoa = |exp, got| format!("wrong number of arguments (expected {exp}, found {got})");
117    let message = match (found, undefined) {
118        ("reduce", Filter(arity)) => wnoa("2", arity),
119        ("foreach", Filter(arity)) => wnoa("2 or 3", arity),
120        (_, undefined) => format!("undefined {}", undefined.as_str()),
121    };
122    let found = [(message.clone(), None)].into();
123
124    Report {
125        message,
126        labels: Vec::from([(found_range, found, Color::Red)]),
127    }
128}
129
130type CodeBlock = codesnake::Block<codesnake::CodeWidth<String>, Box<dyn Display>, Option<Color>>;
131
132/// Function to apply color to snakes/text.
133pub type Paint = fn(&mut Formatter, &Option<Color>, &dyn Display) -> fmt::Result;
134
135struct FromFn<F>(F);
136
137fn from_fn<F: Fn(&mut Formatter) -> fmt::Result>(f: F) -> FromFn<F> {
138    FromFn(f)
139}
140
141impl<F: Fn(&mut Formatter) -> fmt::Result> Display for FromFn<F> {
142    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
143        (self.0)(f)
144    }
145}
146
147impl Report {
148    /// Convert report to a code block.
149    pub fn to_block(&self, idx: &codesnake::LineIndex, paint: Paint) -> CodeBlock {
150        use codesnake::{Block, CodeWidth, Label};
151        let labels = self.labels.iter().cloned().map(|(range, text, color)| {
152            Label::new(range)
153                .with_style(Some(color))
154                .with_text(Box::new(from_fn(move |f| {
155                    text.iter().try_for_each(|(text, col)| paint(f, col, text))
156                })) as Box<dyn Display>)
157        });
158        let block = Block::new(idx, labels).unwrap();
159        block.with_paint(paint).map_code(|c| {
160            let c = c.replace('\t', "    ");
161            let w = unicode_width::UnicodeWidthStr::width(&*c);
162            CodeWidth::new(c, core::cmp::max(w, 1))
163        })
164    }
165}
166
167/// Pretty-printer for file reports.
168pub struct FileReportsDisp<'a, P> {
169    file_reports: &'a FileReports<P>,
170    paint: Paint,
171    path: fn(&P) -> String,
172}
173
174impl<'a, P> FileReportsDisp<'a, P> {
175    /// Construct a new pretty-printer for file reports.
176    ///
177    /// By default, this does not apply any colors and does not print file paths.
178    pub fn new(file_reports: &'a FileReports<P>) -> Self {
179        Self {
180            file_reports,
181            paint: |f, _style, disp| disp.fmt(f),
182            path: |_| "".into(),
183        }
184    }
185
186    /// Set a function that determines how colors should be applied to text.
187    pub fn with_paint(mut self, paint: Paint) -> Self {
188        self.paint = paint;
189        self
190    }
191
192    /// Set a function that determines how the file path should be printed.
193    pub fn with_path(mut self, path: fn(&P) -> String) -> Self {
194        self.path = path;
195        self
196    }
197}
198
199impl<'a, P> Display for FileReportsDisp<'a, P> {
200    fn fmt(&self, f: &mut Formatter) -> fmt::Result {
201        let (file, reports) = &self.file_reports;
202        let path = (self.path)(&file.path);
203        let idx = codesnake::LineIndex::new(&file.code);
204        reports.iter().try_for_each(|e| {
205            writeln!(f, "Error: {}", e.message)?;
206            let block = e.to_block(&idx, self.paint);
207            writeln!(f, "{}{}", block.prologue(), path)?;
208            writeln!(f, "{}", block.space_vert())?;
209            writeln!(f, "{}{}", block, block.epilogue())
210        })
211    }
212}