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 Parsing,
42 Book,
44 Function(Name),
46 Inet(String),
48 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 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 pub fn display_with_severity(&self, severity: Severity) -> impl std::fmt::Display + '_ {
178 DisplayFn(move |f| {
179 let diagnostics = self
202 .diagnostics
203 .iter()
204 .map(|(origin, diags)| (origin, diags.iter().filter(|diag| diag.severity == severity)));
205
206 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 #[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 let only_unknown_file_diagnostics = groups.keys().next_back() == Some(&&None);
224
225 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 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 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 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 pub fn from_byte_span(code: &str, span: Range<usize>) -> Self {
442 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}