bend/
diagnostics.rs

1use TSPL::ParseError;
2
3use crate::fun::{display::DisplayFn, Name, Source};
4use std::{
5  collections::BTreeMap,
6  fmt::{Display, Formatter},
7  ops::Range,
8};
9
10pub const ERR_INDENT_SIZE: usize = 2;
11
12#[derive(Debug, Clone, Default)]
13pub struct Diagnostics {
14  pub diagnostics: BTreeMap<DiagnosticOrigin, Vec<Diagnostic>>,
15  pub config: DiagnosticsConfig,
16}
17
18#[derive(Debug, Clone, Copy)]
19pub struct DiagnosticsConfig {
20  pub verbose: bool,
21  pub irrefutable_match: Severity,
22  pub redundant_match: Severity,
23  pub unreachable_match: Severity,
24  pub unused_definition: Severity,
25  pub repeated_bind: Severity,
26  pub recursion_cycle: Severity,
27  pub missing_main: Severity,
28  pub import_shadow: Severity,
29}
30
31#[derive(Debug, Clone)]
32pub struct Diagnostic {
33  pub message: String,
34  pub severity: Severity,
35  pub source: Source,
36}
37
38#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
39pub enum DiagnosticOrigin {
40  /// An error when parsing source code.
41  Parsing,
42  /// An error from the relationship between multiple top-level definitions.
43  Book,
44  /// An error in a function definition.
45  Function(Name),
46  /// An error in a compiled inet.
47  Inet(String),
48  /// An error during readback of hvm-core run results.
49  Readback,
50}
51
52#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
53pub enum Severity {
54  Allow,
55  Warning,
56  Error,
57}
58
59#[derive(Debug, Clone, Copy)]
60pub enum WarningType {
61  IrrefutableMatch,
62  RedundantMatch,
63  UnreachableMatch,
64  UnusedDefinition,
65  RepeatedBind,
66  RecursionCycle,
67  MissingMain,
68  ImportShadow,
69}
70
71impl Diagnostics {
72  pub fn new(config: DiagnosticsConfig) -> Self {
73    Self { diagnostics: Default::default(), config }
74  }
75
76  pub fn add_parsing_error(&mut self, err: impl std::fmt::Display, source: Source) {
77    self.add_diagnostic(err, Severity::Error, DiagnosticOrigin::Parsing, source);
78  }
79
80  pub fn add_book_error(&mut self, err: impl std::fmt::Display) {
81    self.add_diagnostic(err, Severity::Error, DiagnosticOrigin::Book, Default::default());
82  }
83
84  pub fn add_function_error(&mut self, err: impl std::fmt::Display, name: Name, source: Source) {
85    self.add_diagnostic(
86      err,
87      Severity::Error,
88      DiagnosticOrigin::Function(name.def_name_from_generated()),
89      source,
90    );
91  }
92
93  pub fn add_inet_error(&mut self, err: impl std::fmt::Display, def_name: String) {
94    self.add_diagnostic(err, Severity::Error, DiagnosticOrigin::Inet(def_name), Default::default());
95  }
96
97  pub fn add_function_warning(
98    &mut self,
99    warn: impl std::fmt::Display,
100    warn_type: WarningType,
101    def_name: Name,
102    source: Source,
103  ) {
104    let severity = self.config.warning_severity(warn_type);
105    self.add_diagnostic(
106      warn,
107      severity,
108      DiagnosticOrigin::Function(def_name.def_name_from_generated()),
109      source,
110    );
111  }
112
113  pub fn add_book_warning(&mut self, warn: impl std::fmt::Display, warn_type: WarningType) {
114    let severity = self.config.warning_severity(warn_type);
115    self.add_diagnostic(warn, severity, DiagnosticOrigin::Book, Default::default());
116  }
117
118  pub fn add_diagnostic(
119    &mut self,
120    msg: impl std::fmt::Display,
121    severity: Severity,
122    orig: DiagnosticOrigin,
123    source: Source,
124  ) {
125    let diag = Diagnostic { message: msg.to_string(), severity, source };
126    self.diagnostics.entry(orig).or_default().push(diag)
127  }
128
129  pub fn take_rule_err<T, E: std::fmt::Display>(
130    &mut self,
131    result: Result<T, E>,
132    def_name: Name,
133  ) -> Option<T> {
134    match result {
135      Ok(t) => Some(t),
136      Err(e) => {
137        self.add_function_error(e, def_name, Default::default());
138        None
139      }
140    }
141  }
142
143  pub fn take_inet_err<T, E: std::fmt::Display>(
144    &mut self,
145    result: Result<T, E>,
146    def_name: String,
147  ) -> Option<T> {
148    match result {
149      Ok(t) => Some(t),
150      Err(e) => {
151        self.add_inet_error(e, def_name);
152        None
153      }
154    }
155  }
156
157  pub fn has_severity(&self, severity: Severity) -> bool {
158    self.diagnostics.values().any(|errs| errs.iter().any(|e| e.severity == severity))
159  }
160
161  pub fn has_errors(&self) -> bool {
162    self.has_severity(Severity::Error)
163  }
164
165  /// Checks if any error was emitted since the start of the pass,
166  /// Returning all the current information as a `Err(Info)`, replacing `&mut self` with an empty one.
167  /// Otherwise, returns the given arg as an `Ok(T)`.
168  pub fn fatal<T>(&mut self, t: T) -> Result<T, Diagnostics> {
169    if !self.has_errors() {
170      Ok(t)
171    } else {
172      Err(std::mem::take(self))
173    }
174  }
175
176  /// Returns a Display that prints the diagnostics with one of the given severities.
177  pub fn display_with_severity(&self, severity: Severity) -> impl std::fmt::Display + '_ {
178    DisplayFn(move |f| {
179      // We want to print diagnostics information somewhat like this:
180      // ```
181      // In file A :
182      // In definition X :
183      //   {error}
184      // In definition Y :
185      //   {error}
186      //
187      // In file B :
188      // In compiled Inet Z :
189      //   {error}
190      //
191      // Other diagnostics:
192      // In {...}
193      // ```
194      // The problem is, diagnostics data is currently structured as a mapping from something like
195      // DiagnosticOrigin to Vec<(DiagnosticMessage, DiagnosticFile)>, and we would need something
196      // like a mapping from DiagnosticFile to DiagnosticOrigin to Vec<DiagnosticMessage> in order
197      // to print it cleanly. We might want to change it later to have this structure,
198      // but meanwhile, we do the transformations below to make the goal possible.
199
200      // Ignore diagnostics without the desired severity.
201      let diagnostics = self
202        .diagnostics
203        .iter()
204        .map(|(origin, diags)| (origin, diags.iter().filter(|diag| diag.severity == severity)));
205
206      // Produce the structure described above.
207      let groups: BTreeMap<&Option<String>, BTreeMap<&DiagnosticOrigin, Vec<&Diagnostic>>> = diagnostics
208        .fold(BTreeMap::new(), |mut file_tree, (origin, diags)| {
209          for diag in diags {
210            // We need to allow this Clippy warning due to `Name` in `DiagnosticOrigin::Function`.
211            // We know how it works, so it shouldn't be a problem.
212            #[allow(clippy::mutable_key_type)]
213            let file_group_entry = file_tree.entry(&diag.source.file).or_default();
214            let origin_group_entry = file_group_entry.entry(origin).or_default();
215            origin_group_entry.push(diag);
216          }
217          file_tree
218        });
219      // Now, we have a mapping from DiagnosticFile to DiagnosticOrigin to Vec<DiagnosticMessage>.
220
221      // If the last file is `None`, it means we only have diagnostics with unknown source file.
222      // In this case, we won't print a special message for them.
223      let only_unknown_file_diagnostics = groups.keys().next_back() == Some(&&None);
224
225      // Reverse the group iterator so `None` files go last.
226      for (file, origin_to_diagnostics) in groups.iter().rev() {
227        if !only_unknown_file_diagnostics {
228          match &file {
229            Some(name) => writeln!(f, "\x1b[1mIn \x1b[4m{}\x1b[0m\x1b[1m :\x1b[0m", name)?,
230            None => writeln!(f, "\x1b[1mOther diagnostics:\x1b[0m")?,
231          };
232        }
233
234        let mut has_msg = false;
235        for (origin, diagnostics) in origin_to_diagnostics {
236          let mut diagnostics = diagnostics.iter().peekable();
237          if diagnostics.peek().is_some() {
238            match origin {
239              DiagnosticOrigin::Parsing => {
240                for err in diagnostics {
241                  writeln!(f, "{err}")?;
242                }
243              }
244              DiagnosticOrigin::Book => {
245                for err in diagnostics {
246                  writeln!(f, "{err}")?;
247                }
248              }
249              DiagnosticOrigin::Function(nam) => {
250                writeln!(f, "\x1b[1mIn definition '\x1b[4m{}\x1b[0m\x1b[1m':\x1b[0m", nam)?;
251                for err in diagnostics {
252                  writeln!(f, "{:ERR_INDENT_SIZE$}{err}", "")?;
253                }
254              }
255              DiagnosticOrigin::Inet(nam) => {
256                writeln!(f, "\x1b[1mIn compiled inet '\x1b[4m{}\x1b[0m\x1b[1m':\x1b[0m", nam)?;
257                for err in diagnostics {
258                  writeln!(f, "{:ERR_INDENT_SIZE$}{err}", "")?;
259                }
260              }
261              DiagnosticOrigin::Readback => {
262                writeln!(f, "\x1b[1mDuring readback:\x1b[0m")?;
263                for err in diagnostics {
264                  writeln!(f, "{:ERR_INDENT_SIZE$}{err}", "")?;
265                }
266              }
267            }
268            has_msg = true;
269          }
270        }
271        if has_msg {
272          writeln!(f)?;
273        }
274      }
275      Ok(())
276    })
277  }
278
279  pub fn display_only_messages(&self) -> impl std::fmt::Display + '_ {
280    DisplayFn(move |f| {
281      for err in self.diagnostics.values().flatten() {
282        writeln!(f, "{err}")?;
283      }
284      Ok(())
285    })
286  }
287}
288
289impl Display for Diagnostics {
290  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
291    if self.has_severity(Severity::Warning) {
292      write!(f, "\x1b[4m\x1b[1m\x1b[33mWarnings:\x1b[0m\n{}", self.display_with_severity(Severity::Warning))?;
293    }
294    if self.has_severity(Severity::Error) {
295      write!(f, "\x1b[4m\x1b[1m\x1b[31mErrors:\x1b[0m\n{}", self.display_with_severity(Severity::Error))?;
296    }
297    Ok(())
298  }
299}
300
301impl From<String> for Diagnostics {
302  fn from(value: String) -> Self {
303    Self {
304      diagnostics: BTreeMap::from_iter([(
305        DiagnosticOrigin::Book,
306        vec![Diagnostic { message: value, severity: Severity::Error, source: Default::default() }],
307      )]),
308      ..Default::default()
309    }
310  }
311}
312
313impl From<ParseError> for Diagnostics {
314  /// Transforms a parse error into `Diagnostics`.
315  ///
316  /// NOTE: Since `ParseError` does not include the source code, we can't get the `TextLocation` of the error,
317  /// so it is not included in the diagnostic.
318  /// range is set as None.
319  fn from(value: ParseError) -> Self {
320    Self {
321      diagnostics: BTreeMap::from_iter([(
322        DiagnosticOrigin::Parsing,
323        vec![Diagnostic { message: value.into(), severity: Severity::Error, source: Default::default() }],
324      )]),
325      ..Default::default()
326    }
327  }
328}
329
330impl DiagnosticsConfig {
331  pub fn new(severity: Severity, verbose: bool) -> Self {
332    Self {
333      irrefutable_match: severity,
334      redundant_match: severity,
335      unreachable_match: severity,
336      unused_definition: severity,
337      repeated_bind: severity,
338      recursion_cycle: severity,
339      import_shadow: severity,
340      // Should only be changed manually, as a missing main is always a error to hvm
341      missing_main: Severity::Error,
342      verbose,
343    }
344  }
345
346  pub fn warning_severity(&self, warn: WarningType) -> Severity {
347    match warn {
348      WarningType::UnusedDefinition => self.unused_definition,
349      WarningType::RepeatedBind => self.repeated_bind,
350      WarningType::RecursionCycle => self.recursion_cycle,
351      WarningType::IrrefutableMatch => self.irrefutable_match,
352      WarningType::RedundantMatch => self.redundant_match,
353      WarningType::UnreachableMatch => self.unreachable_match,
354      WarningType::MissingMain => self.missing_main,
355      WarningType::ImportShadow => self.import_shadow,
356    }
357  }
358}
359
360impl Default for DiagnosticsConfig {
361  fn default() -> Self {
362    let mut cfg = Self::new(Severity::Warning, false);
363    cfg.recursion_cycle = Severity::Error;
364    cfg
365  }
366}
367
368impl Display for Diagnostic {
369  fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
370    write!(f, "{}", self.message)
371  }
372}
373
374impl Diagnostic {
375  pub fn display_with_origin<'a>(&'a self, origin: &'a DiagnosticOrigin) -> impl std::fmt::Display + 'a {
376    DisplayFn(move |f| {
377      match origin {
378        DiagnosticOrigin::Parsing => writeln!(f, "{self}")?,
379        DiagnosticOrigin::Book => writeln!(f, "{self}")?,
380        DiagnosticOrigin::Function(nam) => {
381          writeln!(f, "\x1b[1mIn definition '\x1b[4m{}\x1b[0m\x1b[1m':\x1b[0m", nam)?;
382          writeln!(f, "{:ERR_INDENT_SIZE$}{self}", "")?;
383        }
384        DiagnosticOrigin::Inet(nam) => {
385          writeln!(f, "\x1b[1mIn compiled inet '\x1b[4m{}\x1b[0m\x1b[1m':\x1b[0m", nam)?;
386          writeln!(f, "{:ERR_INDENT_SIZE$}{self}", "")?;
387        }
388        DiagnosticOrigin::Readback => {
389          writeln!(f, "\x1b[1mDuring readback:\x1b[0m")?;
390          writeln!(f, "{:ERR_INDENT_SIZE$}{self}", "")?;
391        }
392      };
393      Ok(())
394    })
395  }
396}
397
398#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
399pub struct TextLocation {
400  pub line: usize,
401  pub char: usize,
402}
403
404impl TextLocation {
405  pub fn new(line: usize, char: usize) -> Self {
406    TextLocation { line, char }
407  }
408
409  /// Transforms a `usize` byte index on `code` into a `TextLocation`.
410  pub fn from_byte_loc(code: &str, loc: usize) -> Self {
411    let code = code.as_bytes();
412    let mut line = 0;
413    let mut char = 0;
414    let mut cur_idx = 0;
415    while cur_idx < loc && cur_idx < code.len() {
416      if code[cur_idx] == b'\n' {
417        line += 1;
418        char = 0;
419      } else {
420        char += 1;
421      }
422      cur_idx += 1;
423    }
424
425    TextLocation { line, char }
426  }
427}
428
429#[derive(Debug, Copy, Clone, Hash, PartialEq, PartialOrd, Ord, Eq)]
430pub struct TextSpan {
431  pub start: TextLocation,
432  pub end: TextLocation,
433}
434
435impl TextSpan {
436  pub fn new(start: TextLocation, end: TextLocation) -> Self {
437    TextSpan { start, end }
438  }
439
440  /// Transforms a `usize` byte range on `code` into a `TextLocation`.
441  pub fn from_byte_span(code: &str, span: Range<usize>) -> Self {
442    // Will loop for way too long otherwise
443    assert!(span.start <= span.end);
444
445    let code = code.as_bytes();
446    let mut start_line = 0;
447    let mut start_char = 0;
448    let mut end_line;
449    let mut end_char;
450
451    let mut cur_idx = 0;
452    while cur_idx < span.start && cur_idx < code.len() {
453      if code[cur_idx] == b'\n' {
454        start_line += 1;
455        start_char = 0;
456      } else {
457        start_char += 1;
458      }
459      cur_idx += 1;
460    }
461
462    end_line = start_line;
463    end_char = start_char;
464    while cur_idx < span.end && cur_idx < code.len() {
465      if code[cur_idx] == b'\n' {
466        end_line += 1;
467        end_char = 0;
468      } else {
469        end_char += 1;
470      }
471      cur_idx += 1;
472    }
473
474    TextSpan::new(TextLocation::new(start_line, start_char), TextLocation::new(end_line, end_char))
475  }
476}