1use std::error::Error;
4use std::fmt::{self, Write as _};
5
6use crate::span::Span;
7
8const TAB_WIDTH: usize = 4;
9
10#[non_exhaustive]
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
13#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
14pub enum DiagnosticCode {
15 InvalidToken,
16 UnterminatedString,
17 UnterminatedIdentifier,
18 UnterminatedComment,
19 InvalidEscape,
20 InvalidNumber,
21 UnexpectedToken,
22 UnexpectedEof,
23 ExtraToken,
24 UnclosedDelimiter,
25 MismatchedDelimiter,
26 Recovery,
27 TooManyErrors,
28 UnsupportedSyntax,
29 Internal,
30}
31
32impl DiagnosticCode {
33 #[must_use]
35 pub const fn as_str(self) -> &'static str {
36 match self {
37 Self::InvalidToken => "OCY-L001",
38 Self::UnterminatedString => "OCY-L002",
39 Self::UnterminatedIdentifier => "OCY-L003",
40 Self::UnterminatedComment => "OCY-L004",
41 Self::InvalidEscape => "OCY-L005",
42 Self::InvalidNumber => "OCY-L006",
43 Self::UnexpectedToken => "OCY-P001",
44 Self::UnexpectedEof => "OCY-P002",
45 Self::ExtraToken => "OCY-P003",
46 Self::UnclosedDelimiter => "OCY-P004",
47 Self::MismatchedDelimiter => "OCY-P005",
48 Self::Recovery => "OCY-P006",
49 Self::TooManyErrors => "OCY-P007",
50 Self::UnsupportedSyntax => "OCY-P008",
51 Self::Internal => "OCY-P999",
52 }
53 }
54}
55
56impl fmt::Display for DiagnosticCode {
57 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58 formatter.write_str(self.as_str())
59 }
60}
61
62#[non_exhaustive]
64#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
65#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
66pub enum Severity {
67 Error,
68 Warning,
69 Note,
70}
71
72impl Severity {
73 #[must_use]
75 pub const fn as_str(self) -> &'static str {
76 match self {
77 Self::Error => "error",
78 Self::Warning => "warning",
79 Self::Note => "note",
80 }
81 }
82
83 const fn sort_key(self) -> u8 {
84 match self {
85 Self::Error => 0,
86 Self::Warning => 1,
87 Self::Note => 2,
88 }
89 }
90}
91
92impl fmt::Display for Severity {
93 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
94 formatter.write_str(self.as_str())
95 }
96}
97
98#[derive(Clone, Debug, PartialEq, Eq, Hash)]
100#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
101pub struct Label {
102 pub span: Span,
104 pub message: String,
106}
107
108impl Label {
109 #[must_use]
111 pub fn new(span: Span, message: impl Into<String>) -> Self {
112 Self {
113 span,
114 message: message.into(),
115 }
116 }
117}
118
119#[derive(Clone, Debug, PartialEq, Eq, Hash)]
121#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
122pub struct Diagnostic {
123 pub code: DiagnosticCode,
125 pub severity: Severity,
127 pub message: String,
129 pub primary_span: Span,
131 pub labels: Vec<Label>,
133 pub notes: Vec<String>,
135 pub help: Option<String>,
137}
138
139impl Diagnostic {
140 #[must_use]
142 pub fn new(
143 code: DiagnosticCode,
144 severity: Severity,
145 message: impl Into<String>,
146 primary_span: Span,
147 ) -> Self {
148 Self {
149 code,
150 severity,
151 message: message.into(),
152 primary_span,
153 labels: Vec::new(),
154 notes: Vec::new(),
155 help: None,
156 }
157 }
158
159 #[must_use]
161 pub fn error(code: DiagnosticCode, message: impl Into<String>, primary_span: Span) -> Self {
162 Self::new(code, Severity::Error, message, primary_span)
163 }
164
165 #[must_use]
167 pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
168 self.labels.push(Label::new(span, message));
169 self
170 }
171
172 #[must_use]
174 pub fn with_note(mut self, note: impl Into<String>) -> Self {
175 self.notes.push(note.into());
176 self
177 }
178
179 #[must_use]
181 pub fn with_help(mut self, help: impl Into<String>) -> Self {
182 self.help = Some(help.into());
183 self
184 }
185
186 #[must_use]
188 pub const fn is_error(&self) -> bool {
189 matches!(self.severity, Severity::Error)
190 }
191
192 #[must_use]
197 pub fn render(&self, source: &str) -> String {
198 let mut rendered = String::new();
199 render_one(&mut rendered, source, self).expect("writing to a String cannot fail");
200 rendered
201 }
202}
203
204impl fmt::Display for Diagnostic {
205 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
206 write!(
207 formatter,
208 "{}[{}]: {} at {}",
209 self.severity, self.code, self.message, self.primary_span
210 )
211 }
212}
213
214#[derive(Clone, Debug, PartialEq, Eq)]
216#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
217pub struct ParseErrors {
218 diagnostics: Vec<Diagnostic>,
219}
220
221impl ParseErrors {
222 #[must_use]
230 pub fn new(mut diagnostics: Vec<Diagnostic>) -> Self {
231 assert!(
232 !diagnostics.is_empty(),
233 "ParseErrors requires at least one diagnostic"
234 );
235 sort_diagnostics(&mut diagnostics);
236 Self { diagnostics }
237 }
238
239 #[must_use]
241 pub fn try_new(diagnostics: Vec<Diagnostic>) -> Option<Self> {
242 if diagnostics.is_empty() {
243 None
244 } else {
245 Some(Self::new(diagnostics))
246 }
247 }
248
249 #[must_use]
251 pub fn diagnostics(&self) -> &[Diagnostic] {
252 &self.diagnostics
253 }
254
255 #[must_use]
257 pub fn into_diagnostics(self) -> Vec<Diagnostic> {
258 self.diagnostics
259 }
260
261 #[must_use]
263 pub fn len(&self) -> usize {
264 self.diagnostics.len()
265 }
266
267 #[must_use]
273 pub fn is_empty(&self) -> bool {
274 self.diagnostics.is_empty()
275 }
276
277 #[must_use]
279 pub fn render(&self, source: &str) -> String {
280 let mut rendered = String::new();
281 for (index, diagnostic) in self.diagnostics.iter().enumerate() {
282 if index != 0 {
283 rendered.push_str("\n\n");
284 }
285 render_one(&mut rendered, source, diagnostic).expect("writing to a String cannot fail");
286 }
287 rendered
288 }
289}
290
291impl From<Diagnostic> for ParseErrors {
292 fn from(diagnostic: Diagnostic) -> Self {
293 Self::new(vec![diagnostic])
294 }
295}
296
297impl AsRef<[Diagnostic]> for ParseErrors {
298 fn as_ref(&self) -> &[Diagnostic] {
299 self.diagnostics()
300 }
301}
302
303impl<'errors> IntoIterator for &'errors ParseErrors {
304 type Item = &'errors Diagnostic;
305 type IntoIter = std::slice::Iter<'errors, Diagnostic>;
306
307 fn into_iter(self) -> Self::IntoIter {
308 self.diagnostics.iter()
309 }
310}
311
312impl IntoIterator for ParseErrors {
313 type Item = Diagnostic;
314 type IntoIter = std::vec::IntoIter<Diagnostic>;
315
316 fn into_iter(self) -> Self::IntoIter {
317 self.diagnostics.into_iter()
318 }
319}
320
321impl fmt::Display for ParseErrors {
322 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
323 let Some(first) = self.diagnostics.first() else {
324 return formatter.write_str("no parse diagnostics");
325 };
326 if self.diagnostics.len() == 1 {
327 first.fmt(formatter)
328 } else {
329 write!(
330 formatter,
331 "{} (and {} more diagnostic{})",
332 first,
333 self.diagnostics.len() - 1,
334 if self.diagnostics.len() == 2 { "" } else { "s" }
335 )
336 }
337 }
338}
339
340impl Error for ParseErrors {}
341
342fn sort_diagnostics(diagnostics: &mut [Diagnostic]) {
343 diagnostics.sort_by(|left, right| {
344 (
345 left.primary_span.start,
346 left.primary_span.end,
347 left.severity.sort_key(),
348 left.code,
349 left.message.as_str(),
350 )
351 .cmp(&(
352 right.primary_span.start,
353 right.primary_span.end,
354 right.severity.sort_key(),
355 right.code,
356 right.message.as_str(),
357 ))
358 });
359}
360
361fn render_one(output: &mut String, source: &str, diagnostic: &Diagnostic) -> fmt::Result {
362 writeln!(
363 output,
364 "{}[{}]: {}",
365 diagnostic.severity, diagnostic.code, diagnostic.message
366 )?;
367
368 let primary = snippet(source, diagnostic.primary_span);
369 writeln!(output, " --> {}:{}", primary.line_number, primary.column)?;
370 writeln!(output, " |")?;
371
372 let primary_label = diagnostic
373 .labels
374 .iter()
375 .find(|label| label.span == diagnostic.primary_span)
376 .map(|label| label.message.as_str())
377 .unwrap_or("");
378 render_snippet(output, &primary, '^', primary_label)?;
379
380 let mut labels: Vec<_> = diagnostic
381 .labels
382 .iter()
383 .filter(|label| label.span != diagnostic.primary_span)
384 .collect();
385 labels.sort_by(|left, right| {
386 (left.span.start, left.span.end, left.message.as_str()).cmp(&(
387 right.span.start,
388 right.span.end,
389 right.message.as_str(),
390 ))
391 });
392
393 for label in labels {
394 writeln!(output, " |")?;
395 let secondary = snippet(source, label.span);
396 render_snippet(output, &secondary, '-', &label.message)?;
397 }
398
399 for note in &diagnostic.notes {
400 writeln!(output, " = note: {note}")?;
401 }
402 if let Some(help) = &diagnostic.help {
403 writeln!(output, " = help: {help}")?;
404 }
405
406 if output.ends_with('\n') {
407 output.pop();
408 }
409 Ok(())
410}
411
412fn render_snippet(
413 output: &mut String,
414 snippet: &Snippet,
415 marker: char,
416 label: &str,
417) -> fmt::Result {
418 let gutter_width = decimal_width(snippet.line_number);
419 writeln!(
420 output,
421 "{:>gutter_width$} | {}",
422 snippet.line_number, snippet.line
423 )?;
424 write!(
425 output,
426 "{:>gutter_width$} | {}{}",
427 "",
428 " ".repeat(snippet.marker_start),
429 marker.to_string().repeat(snippet.marker_len)
430 )?;
431 if !label.is_empty() {
432 write!(output, " {label}")?;
433 }
434 if snippet.continues {
435 write!(output, " (continues on the next line)")?;
436 }
437 writeln!(output)
438}
439
440struct Snippet {
441 line_number: usize,
442 column: usize,
443 line: String,
444 marker_start: usize,
445 marker_len: usize,
446 continues: bool,
447}
448
449fn snippet(source: &str, span: Span) -> Snippet {
450 let start = floor_char_boundary(source, span.start.min(source.len()));
451 let requested_end = floor_char_boundary(source, span.end.min(source.len()));
452 let before = &source[..start];
453 let line_start = before.rfind('\n').map_or(0, |index| index + 1);
454 let line_number = before.bytes().filter(|byte| *byte == b'\n').count() + 1;
455 let mut line_end = source[start..]
456 .find('\n')
457 .map_or(source.len(), |offset| start + offset);
458 if line_end > line_start && source.as_bytes()[line_end - 1] == b'\r' {
459 line_end -= 1;
460 }
461
462 let visible_start = start.min(line_end);
463 let visible_end = requested_end.min(line_end).max(visible_start);
464 let marker_start = display_width(&source[line_start..visible_start]);
465 let marker_len = display_width(&source[visible_start..visible_end]).max(1);
466
467 Snippet {
468 line_number,
469 column: marker_start + 1,
470 line: expand_tabs(&source[line_start..line_end]),
471 marker_start,
472 marker_len,
473 continues: requested_end > line_end,
474 }
475}
476
477fn floor_char_boundary(source: &str, mut offset: usize) -> usize {
478 while offset > 0 && !source.is_char_boundary(offset) {
479 offset -= 1;
480 }
481 offset
482}
483
484fn display_width(text: &str) -> usize {
485 let mut column = 0;
486 for character in text.chars() {
487 if character == '\t' {
488 column += TAB_WIDTH - (column % TAB_WIDTH);
489 } else {
490 column += 1;
491 }
492 }
493 column
494}
495
496fn expand_tabs(text: &str) -> String {
497 let mut expanded = String::with_capacity(text.len());
498 let mut column = 0;
499 for character in text.chars() {
500 if character == '\t' {
501 let spaces = TAB_WIDTH - (column % TAB_WIDTH);
502 expanded.extend(std::iter::repeat_n(' ', spaces));
503 column += spaces;
504 } else {
505 expanded.push(character);
506 column += 1;
507 }
508 }
509 expanded
510}
511
512fn decimal_width(mut value: usize) -> usize {
513 let mut width = 1;
514 while value >= 10 {
515 value /= 10;
516 width += 1;
517 }
518 width
519}