Skip to main content

zerodds_idl/
errors.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Crate-weites Diagnostik-Grundgeruest.
4//!
5//! Drei Schichten:
6//!
7//! - [`Span`] — Byte-Offset-Bereich im Source-Text. Klein, `Copy`,
8//!   trivial mergebar.
9//! - [`Diagnostic`] — strukturierte Fehler-/Warnungs-Meldung mit
10//!   primary span, optional secondary spans (rustc-inspiriert).
11//! - [`ParseError`] — domaenenspezifische Fehler-Familie, die spaeter
12//!   vom Lexer (Task 2.x) und der Engine konsumiert wird; `to_diagnostic`
13//!   konvertiert in das uniforme Anzeige-Format.
14//!
15//! Diese Schicht ist Grundgeruest: die Datentypen stehen, aber Lexer und
16//! Engine produzieren noch keine [`ParseError`]-Werte. Die Anbindung
17//! erfolgt schrittweise in Woche 2, wenn der Lexer mit Spans arbeitet
18//! und der Recognizer auf Token-Wrapper (statt nackten [`crate::grammar::TokenKind`])
19//! umgestellt wird.
20//!
21//! Siehe RFC 0001 §5.5 (Error-Reporting).
22
23use core::fmt;
24
25use crate::grammar::TokenKind;
26
27/// Byte-Offset-Bereich `[start, end)` im Source-Text.
28///
29/// Halb-offenes Intervall: `start` inklusive, `end` exklusive. Eine
30/// Zero-length-Span (`start == end`) markiert eine Position ohne Inhalt
31/// — typisch fuer EOF-Caret oder Insertion-Vorschlaege.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
33pub struct Span {
34    /// Startposition (inklusive, byte-offset).
35    pub start: usize,
36    /// Endposition (exklusive, byte-offset).
37    pub end: usize,
38}
39
40impl Span {
41    /// Synthetische Span ohne Quellort. Wird fuer interne, nicht-quellbasierte
42    /// Diagnostiken verwendet (z.B. Grammar-Konstruktionsfehler).
43    pub const SYNTHETIC: Self = Self { start: 0, end: 0 };
44
45    /// Konstruiert eine neue Span aus Start- und End-Offset.
46    #[must_use]
47    pub const fn new(start: usize, end: usize) -> Self {
48        Self { start, end }
49    }
50
51    /// Span mit Laenge 0 an einer Position (z.B. EOF-Marker).
52    #[must_use]
53    pub const fn point(at: usize) -> Self {
54        Self { start: at, end: at }
55    }
56
57    /// Anzahl der ueberdeckten Bytes.
58    #[must_use]
59    pub const fn len(&self) -> usize {
60        self.end.saturating_sub(self.start)
61    }
62
63    /// `true`, wenn Start == End (Zero-length).
64    #[must_use]
65    pub const fn is_empty(&self) -> bool {
66        self.start == self.end
67    }
68
69    /// Liefert die kleinste Span, die beide ueberdeckt. Symmetrisch.
70    #[must_use]
71    pub const fn merge(self, other: Self) -> Self {
72        Self {
73            start: if self.start < other.start {
74                self.start
75            } else {
76                other.start
77            },
78            end: if self.end > other.end {
79                self.end
80            } else {
81                other.end
82            },
83        }
84    }
85
86    /// `true`, wenn `byte_offset` innerhalb dieser Span liegt.
87    #[must_use]
88    pub const fn contains_offset(&self, byte_offset: usize) -> bool {
89        byte_offset >= self.start && byte_offset < self.end
90    }
91}
92
93impl fmt::Display for Span {
94    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
95        write!(f, "{}..{}", self.start, self.end)
96    }
97}
98
99/// Schweregrad einer [`Diagnostic`]. Entspricht den rustc-Standardstufen.
100#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
101pub enum Severity {
102    /// Hilfreicher Hinweis (z.B. Vorschlag einer Korrektur).
103    Help,
104    /// Kontextuelle Anmerkung zu einer anderen Diagnostik.
105    Note,
106    /// Warnung — Code laeuft, koennte aber problematisch sein.
107    Warning,
108    /// Fehler — Verarbeitung kann nicht fortgesetzt werden.
109    Error,
110}
111
112impl fmt::Display for Severity {
113    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
114        let label = match self {
115            Self::Help => "help",
116            Self::Note => "note",
117            Self::Warning => "warning",
118            Self::Error => "error",
119        };
120        f.write_str(label)
121    }
122}
123
124/// Zusaetzliche Span mit erlaeuterndem Text (sekundaerer Anker einer
125/// [`Diagnostic`]).
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Label {
128    /// Span im Source-Text.
129    pub span: Span,
130    /// Erlaeuterungs-Text (wird neben dem Span angezeigt).
131    pub message: String,
132}
133
134/// Strukturierte Diagnose-Meldung.
135///
136/// Modelliert nach rustc-Diagnostics: ein Schweregrad, eine Hauptbotschaft,
137/// ein primary-Span und beliebig viele zusaetzliche Labels mit eigenem Text.
138#[derive(Debug, Clone, PartialEq, Eq)]
139pub struct Diagnostic {
140    /// Schweregrad.
141    pub severity: Severity,
142    /// Kompakter Text (eine Zeile, ohne Punkt am Ende).
143    pub message: String,
144    /// Primary-Anker im Source-Text.
145    pub primary_span: Span,
146    /// Zusaetzliche Anker mit Erlaeuterungen.
147    pub labels: Vec<Label>,
148}
149
150impl Diagnostic {
151    /// Konstruktor fuer Fehler-Diagnostiken.
152    #[must_use]
153    pub fn error(message: impl Into<String>, primary_span: Span) -> Self {
154        Self {
155            severity: Severity::Error,
156            message: message.into(),
157            primary_span,
158            labels: Vec::new(),
159        }
160    }
161
162    /// Konstruktor fuer Warnung-Diagnostiken.
163    #[must_use]
164    pub fn warning(message: impl Into<String>, primary_span: Span) -> Self {
165        Self {
166            severity: Severity::Warning,
167            message: message.into(),
168            primary_span,
169            labels: Vec::new(),
170        }
171    }
172
173    /// Builder-Methode: zusaetzliches Label anhaengen.
174    #[must_use]
175    pub fn with_label(mut self, span: Span, message: impl Into<String>) -> Self {
176        self.labels.push(Label {
177            span,
178            message: message.into(),
179        });
180        self
181    }
182}
183
184/// Domaenenspezifische Parser-Fehler.
185///
186/// Wird vom Lexer (Woche 2), Recognizer (spaetere Anbindung), CST-Builder
187/// und AST-Builder konsumiert. Jede Variante laesst sich via
188/// [`ParseError::to_diagnostic`] in eine [`Diagnostic`] ueberfuehren.
189#[derive(Debug, Clone, PartialEq, Eq)]
190pub enum ParseError {
191    /// Erwartet wurde eine bestimmte Token-Menge, gefunden wurde ein
192    /// anderes Token.
193    UnexpectedToken {
194        /// Was tatsaechlich gelesen wurde.
195        found: TokenKind,
196        /// Welche Tokens erwartet waren (aus dem Earley-Expected-Set).
197        expected: Vec<TokenKind>,
198        /// Position des unerwarteten Tokens.
199        span: Span,
200    },
201    /// Eingabe endete vor Abschluss der Production. Erwartet wurde noch
202    /// mindestens ein Token.
203    UnexpectedEof {
204        /// Was an dieser Stelle erwartet war.
205        expected: Vec<TokenKind>,
206        /// Endposition der Eingabe.
207        span: Span,
208    },
209    /// Lexer-spezifischer Fehler (ungueltige Zeichen-Sequenz, unterminated
210    /// String, etc.). Detail-Text ist lexer-spezifisch.
211    LexerError {
212        /// Beschreibung des Lexer-Problems.
213        message: String,
214        /// Position der fehlerhaften Sequenz.
215        span: Span,
216    },
217}
218
219impl ParseError {
220    /// Konvertiert einen Parser-Fehler in eine uniforme [`Diagnostic`].
221    #[must_use]
222    pub fn to_diagnostic(&self) -> Diagnostic {
223        match self {
224            Self::UnexpectedToken {
225                found,
226                expected,
227                span,
228            } => {
229                let msg = format!(
230                    "unexpected token {:?}, expected {}",
231                    found,
232                    format_expected(expected),
233                );
234                Diagnostic::error(msg, *span)
235            }
236            Self::UnexpectedEof { expected, span } => {
237                let msg = format!(
238                    "unexpected end of input, expected {}",
239                    format_expected(expected),
240                );
241                Diagnostic::error(msg, *span)
242            }
243            Self::LexerError { message, span } => Diagnostic::error(message.clone(), *span),
244        }
245    }
246
247    /// Position des Fehlers im Source-Text.
248    #[must_use]
249    pub const fn span(&self) -> Span {
250        match self {
251            Self::UnexpectedToken { span, .. }
252            | Self::UnexpectedEof { span, .. }
253            | Self::LexerError { span, .. } => *span,
254        }
255    }
256}
257
258/// Formatiert eine Liste erwarteter Tokens fuer die Anzeige in
259/// Diagnostik-Texten.
260fn format_expected(expected: &[TokenKind]) -> String {
261    match expected {
262        [] => "(nothing)".to_string(),
263        [single] => format!("{single:?}"),
264        many => {
265            let formatted: Vec<String> = many.iter().map(|t| format!("{t:?}")).collect();
266            format!("one of [{}]", formatted.join(", "))
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::*;
274
275    // -----------------------------------------------------------------
276    // Span
277    // -----------------------------------------------------------------
278
279    #[test]
280    fn span_new_stores_start_and_end() {
281        let s = Span::new(3, 7);
282        assert_eq!(s.start, 3);
283        assert_eq!(s.end, 7);
284    }
285
286    #[test]
287    fn span_point_has_zero_length() {
288        let s = Span::point(42);
289        assert_eq!(s.start, 42);
290        assert_eq!(s.end, 42);
291        assert!(s.is_empty());
292        assert_eq!(s.len(), 0);
293    }
294
295    #[test]
296    fn span_len_counts_bytes() {
297        assert_eq!(Span::new(0, 5).len(), 5);
298        assert_eq!(Span::new(10, 14).len(), 4);
299    }
300
301    #[test]
302    fn span_merge_takes_outer_bounds() {
303        let a = Span::new(2, 5);
304        let b = Span::new(4, 8);
305        assert_eq!(a.merge(b), Span::new(2, 8));
306        // Symmetrisch.
307        assert_eq!(b.merge(a), Span::new(2, 8));
308    }
309
310    #[test]
311    fn span_merge_with_synthetic_keeps_real_bounds() {
312        let real = Span::new(10, 20);
313        // SYNTHETIC ist 0..0; merge nimmt min(start)=0 — das ist semantisch
314        // erwartet (synthetic verbreitert nach 0). Als Doku-Anker.
315        let merged = real.merge(Span::SYNTHETIC);
316        assert_eq!(merged, Span::new(0, 20));
317    }
318
319    #[test]
320    fn span_contains_offset_is_half_open() {
321        let s = Span::new(5, 10);
322        assert!(s.contains_offset(5));
323        assert!(s.contains_offset(7));
324        assert!(s.contains_offset(9));
325        assert!(!s.contains_offset(10), "end ist exklusiv");
326        assert!(!s.contains_offset(4));
327    }
328
329    #[test]
330    fn span_displays_as_start_dot_dot_end() {
331        assert_eq!(format!("{}", Span::new(3, 7)), "3..7");
332    }
333
334    // -----------------------------------------------------------------
335    // Severity
336    // -----------------------------------------------------------------
337
338    #[test]
339    fn severity_orders_help_lower_than_error() {
340        assert!(Severity::Help < Severity::Note);
341        assert!(Severity::Note < Severity::Warning);
342        assert!(Severity::Warning < Severity::Error);
343    }
344
345    #[test]
346    fn severity_displays_lowercase_label() {
347        assert_eq!(format!("{}", Severity::Error), "error");
348        assert_eq!(format!("{}", Severity::Warning), "warning");
349        assert_eq!(format!("{}", Severity::Note), "note");
350        assert_eq!(format!("{}", Severity::Help), "help");
351    }
352
353    // -----------------------------------------------------------------
354    // Diagnostic
355    // -----------------------------------------------------------------
356
357    #[test]
358    fn diagnostic_error_constructor_sets_severity() {
359        let d = Diagnostic::error("oops", Span::new(0, 3));
360        assert_eq!(d.severity, Severity::Error);
361        assert_eq!(d.message, "oops");
362        assert_eq!(d.primary_span, Span::new(0, 3));
363        assert!(d.labels.is_empty());
364    }
365
366    #[test]
367    fn diagnostic_warning_constructor_sets_severity() {
368        let d = Diagnostic::warning("hmm", Span::new(2, 4));
369        assert_eq!(d.severity, Severity::Warning);
370    }
371
372    #[test]
373    fn diagnostic_with_label_appends_in_order() {
374        let d = Diagnostic::error("bad", Span::new(0, 3))
375            .with_label(Span::new(5, 10), "see here")
376            .with_label(Span::new(20, 22), "and here");
377        assert_eq!(d.labels.len(), 2);
378        assert_eq!(d.labels[0].message, "see here");
379        assert_eq!(d.labels[0].span, Span::new(5, 10));
380        assert_eq!(d.labels[1].message, "and here");
381    }
382
383    // -----------------------------------------------------------------
384    // ParseError
385    // -----------------------------------------------------------------
386
387    #[test]
388    fn parse_error_unexpected_token_to_diagnostic() {
389        let err = ParseError::UnexpectedToken {
390            found: TokenKind::Keyword("struct"),
391            expected: vec![TokenKind::Punct("{")],
392            span: Span::new(7, 13),
393        };
394        let d = err.to_diagnostic();
395        assert_eq!(d.severity, Severity::Error);
396        assert!(d.message.contains("unexpected token"));
397        assert!(d.message.contains("struct"));
398        assert!(d.message.contains("{"));
399        assert_eq!(d.primary_span, Span::new(7, 13));
400    }
401
402    #[test]
403    fn parse_error_unexpected_eof_to_diagnostic() {
404        let err = ParseError::UnexpectedEof {
405            expected: vec![TokenKind::Punct(";")],
406            span: Span::point(50),
407        };
408        let d = err.to_diagnostic();
409        assert!(d.message.contains("end of input"));
410        assert_eq!(d.primary_span, Span::point(50));
411    }
412
413    #[test]
414    fn parse_error_lexer_to_diagnostic_uses_message() {
415        let err = ParseError::LexerError {
416            message: "unterminated string literal".to_string(),
417            span: Span::new(10, 12),
418        };
419        let d = err.to_diagnostic();
420        assert_eq!(d.message, "unterminated string literal");
421    }
422
423    #[test]
424    fn parse_error_span_accessor_returns_inner_span() {
425        let err = ParseError::UnexpectedToken {
426            found: TokenKind::Ident,
427            expected: vec![],
428            span: Span::new(3, 6),
429        };
430        assert_eq!(err.span(), Span::new(3, 6));
431    }
432
433    #[test]
434    fn format_expected_handles_empty_single_and_many() {
435        assert_eq!(format_expected(&[]), "(nothing)");
436        assert_eq!(format_expected(&[TokenKind::Ident]), "Ident");
437        let many = format_expected(&[TokenKind::Punct(";"), TokenKind::Punct(",")]);
438        assert!(many.contains("one of"));
439        assert!(many.contains(';'));
440        assert!(many.contains(','));
441    }
442}