slang_shared/
diagnostic_engine.rs

1use colored::Colorize;
2use slang_error::{CompilerError, ErrorCode, LineInfo};
3use slang_ir::location::Location;
4
5/// Represents the severity level of a diagnostic message
6#[derive(Debug, Clone)]
7pub enum ErrorSeverity {
8    /// A compilation error that prevents successful compilation
9    Error,
10    /// A warning that doesn't prevent compilation but may indicate issues
11    Warning,
12    /// An informational note providing additional context
13    Note,
14}
15
16/// Represents a single diagnostic message with context and suggestions
17#[derive(Debug, Clone)]
18pub struct Diagnostic {
19    /// The severity level of this diagnostic
20    pub severity: ErrorSeverity,
21    /// The structured error code for this diagnostic
22    pub error_code: ErrorCode,
23    /// The human-readable message describing the issue
24    pub message: String,
25    /// The source location where this diagnostic occurred
26    pub location: Location,
27    /// Optional suggestions for fixing the issue
28    pub suggestions: Vec<Suggestion>,
29    /// Related diagnostics that provide additional context
30    pub related: Vec<Diagnostic>,
31}
32
33/// Represents a suggestion for fixing a diagnostic issue
34#[derive(Debug, Clone)]
35pub struct Suggestion {
36    /// The suggestion message explaining how to fix the issue
37    pub message: String,
38    /// Optional replacement text for the problematic code
39    pub replacement: Option<String>,
40    /// Optional location where the replacement should be applied
41    pub location: Option<Location>,
42}
43
44/// A diagnostic collection and reporting engine for compiler errors, warnings, and notes
45///
46/// The DiagnosticEngine serves as the central hub for collecting, managing, and reporting
47/// all kinds of diagnostic messages during compilation. It supports error recovery mode,
48/// rich formatting with source code context, and can emit diagnostics in various formats.
49///
50/// ### Features
51/// - Collects errors, warnings, and notes with source location information
52/// - Supports error recovery mode for collecting multiple errors in one pass
53/// - Rich formatting with colored output and source code context
54/// - Configurable error limits to prevent overwhelming output
55/// - Integration with CompilerError for unified error handling
56///
57/// ### Example
58/// ```rust
59/// use slang_shared::DiagnosticEngine;
60/// use slang_error::ErrorCode;
61/// use slang_ir::location::Location;
62///
63/// let mut engine = DiagnosticEngine::new();
64/// engine.set_file_name("example.sl".to_string());
65/// engine.emit_error(
66///     ErrorCode::ExpectedSemicolon,
67///     "Missing semicolon".to_string(),
68///     Location::new(42, 5, 10, 1)
69/// );
70///
71/// if engine.has_errors() {
72///     engine.report_all(&source_code);
73/// }
74/// ```
75pub struct DiagnosticEngine<'a> {
76    diagnostics: Vec<Diagnostic>,
77    error_count: usize,
78    warning_count: usize,
79    max_errors: usize,
80    recovery_mode: bool,
81    file_name: Option<String>,
82    source_text: Option<&'a str>,
83}
84
85impl<'a> DiagnosticEngine<'a> {
86    /// Creates a new diagnostic engine with default settings
87    ///
88    /// ### Returns
89    /// A new DiagnosticEngine instance ready for collecting diagnostics
90    ///
91    /// ### Example
92    /// ```rust
93    /// use slang_shared::DiagnosticEngine;
94    ///
95    /// let engine = DiagnosticEngine::new();
96    /// ```
97    pub fn new() -> Self {
98        Self {
99            diagnostics: Vec::new(),
100            error_count: 0,
101            warning_count: 0,
102            max_errors: 100,
103            recovery_mode: false,
104            file_name: None,
105            source_text: None,
106        }
107    }
108
109    /// Emits a diagnostic message to the engine
110    ///
111    /// This is the core method for adding diagnostics. It handles error counting,
112    /// enforces error limits, and manages the diagnostic collection.
113    ///
114    /// ### Arguments
115    /// * `diagnostic` - The diagnostic to emit
116    ///
117    /// ### Example
118    /// ```rust
119    /// use slang_shared::{DiagnosticEngine, Diagnostic, ErrorSeverity};
120    /// use slang_error::ErrorCode;
121    /// use slang_ir::location::Location;
122    ///
123    /// let mut engine = DiagnosticEngine::new();
124    /// let diagnostic = Diagnostic {
125    ///     severity: ErrorSeverity::Error,
126    ///     error_code: ErrorCode::ExpectedSemicolon,
127    ///     message: "Missing semicolon".to_string(),
128    ///     location: Location::new(42, 5, 10, 1),
129    ///     suggestions: Vec::new(),
130    ///     related: Vec::new(),
131    /// };
132    /// engine.emit(diagnostic);
133    /// ```
134    pub fn emit(&mut self, diagnostic: Diagnostic) {
135        match diagnostic.severity {
136            ErrorSeverity::Error => {
137                self.error_count += 1;
138                if self.error_count >= self.max_errors {
139                    self.emit_too_many_errors();
140                    return;
141                }
142            }
143            ErrorSeverity::Warning => self.warning_count += 1,
144            ErrorSeverity::Note => {}
145        }
146        self.diagnostics.push(diagnostic);
147    }
148
149    /// Emits an error diagnostic with the specified details
150    ///
151    /// This is a convenience method for creating and emitting error diagnostics.
152    ///
153    /// ### Arguments
154    /// * `error_code` - The structured error code
155    /// * `message` - The error message
156    /// * `location` - The source location where the error occurred
157    ///
158    /// ### Example
159    /// ```rust
160    /// use slang_shared::DiagnosticEngine;
161    /// use slang_error::ErrorCode;
162    /// use slang_ir::location::Location;
163    ///
164    /// let mut engine = DiagnosticEngine::new();
165    /// engine.emit_error(
166    ///     ErrorCode::ExpectedSemicolon,
167    ///     "Missing semicolon after statement".to_string(),
168    ///     Location::new(42, 5, 10, 1)
169    /// );
170    /// ```
171    pub fn emit_error(&mut self, error_code: ErrorCode, message: String, location: Location) {
172        self.emit(Diagnostic {
173            severity: ErrorSeverity::Error,
174            error_code,
175            message,
176            location,
177            suggestions: Vec::new(),
178            related: Vec::new(),
179        });
180    }
181
182    /// Emits a warning diagnostic with the specified details
183    ///
184    /// This is a convenience method for creating and emitting warning diagnostics.
185    ///
186    /// ### Arguments
187    /// * `error_code` - The structured error code
188    /// * `message` - The warning message
189    /// * `location` - The source location where the warning occurred
190    ///
191    /// ### Example
192    /// ```rust
193    /// use slang_shared::DiagnosticEngine;
194    /// use slang_error::ErrorCode;
195    /// use slang_ir::location::Location;
196    ///
197    /// let mut engine = DiagnosticEngine::new();
198    /// engine.emit_warning(
199    ///     ErrorCode::UnusedVariable,
200    ///     "Variable 'x' is declared but never used".to_string(),
201    ///     Location::new(15, 3, 5, 1)
202    /// );
203    /// ```
204    pub fn emit_warning(&mut self, error_code: ErrorCode, message: String, location: Location) {
205        self.emit(Diagnostic {
206            severity: ErrorSeverity::Warning,
207            error_code,
208            message,
209            location,
210            suggestions: Vec::new(),
211            related: Vec::new(),
212        });
213    }
214
215    /// Emits an error diagnostic with a suggestion for fixing the issue
216    ///
217    /// This is useful for providing actionable feedback to users about how to fix errors.
218    ///
219    /// ### Arguments
220    /// * `error_code` - The structured error code
221    /// * `message` - The error message
222    /// * `location` - The source location where the error occurred
223    /// * `suggestion` - A suggestion for fixing the error
224    ///
225    /// ### Example
226    /// ```rust
227    /// use slang_shared::{DiagnosticEngine, Suggestion};
228    /// use slang_error::ErrorCode;
229    /// use slang_ir::location::Location;
230    ///
231    /// let mut engine = DiagnosticEngine::new();
232    /// let suggestion = Suggestion {
233    ///     message: "Add a semicolon".to_string(),
234    ///     replacement: Some(";".to_string()),
235    ///     location: Some(Location::new(42, 5, 10, 0)),
236    /// };
237    /// engine.emit_with_suggestion(
238    ///     ErrorCode::ExpectedSemicolon,
239    ///     "Missing semicolon".to_string(),
240    ///     Location::new(42, 5, 10, 1),
241    ///     suggestion
242    /// );
243    /// ```
244    pub fn emit_with_suggestion(
245        &mut self,
246        error_code: ErrorCode,
247        message: String,
248        location: Location,
249        suggestion: Suggestion,
250    ) {
251        self.emit(Diagnostic {
252            severity: ErrorSeverity::Error,
253            error_code,
254            message,
255            location,
256            suggestions: vec![suggestion],
257            related: Vec::new(),
258        });
259    }
260
261    /// Directly emits a CompilerError as a diagnostic
262    ///
263    /// This method provides seamless integration with the existing CompilerError type,
264    /// allowing for unified error handling across the compiler pipeline.
265    ///
266    /// ### Arguments
267    /// * `error` - The CompilerError to emit as a diagnostic
268    ///
269    /// ### Example
270    /// ```rust
271    /// use slang_shared::DiagnosticEngine;
272    /// use slang_error::{CompilerError, ErrorCode};
273    ///
274    /// let mut engine = DiagnosticEngine::new();
275    /// let error = CompilerError::new(
276    ///     ErrorCode::ExpectedSemicolon,
277    ///     "Missing semicolon".to_string(),
278    ///     5, 10, 42, Some(1)
279    /// );
280    /// engine.emit_compiler_error(error);
281    /// ```
282    pub fn emit_compiler_error(&mut self, error: CompilerError) {
283        let diagnostic = Diagnostic {
284            severity: ErrorSeverity::Error,
285            error_code: error.error_code,
286            message: error.message.clone(),
287            location: Location::new(
288                error.position,
289                error.line,
290                error.column,
291                error.token_length.unwrap_or(1),
292            ),
293            suggestions: Vec::new(),
294            related: Vec::new(),
295        };
296        self.emit(diagnostic);
297    }
298
299    /// Retrieves all error diagnostics as CompilerError instances
300    ///
301    /// This method converts all error-level diagnostics back to CompilerError format,
302    /// useful for interfacing with code that expects the traditional CompilerError type.
303    ///
304    /// ### Returns
305    /// A vector of CompilerError instances representing all errors collected
306    ///
307    /// ### Example
308    /// ```rust
309    /// use slang_shared::DiagnosticEngine;
310    /// use slang_error::ErrorCode;
311    /// use slang_ir::location::Location;
312    ///
313    /// let mut engine = DiagnosticEngine::new();
314    /// engine.emit_error(
315    ///     ErrorCode::ExpectedSemicolon,
316    ///     "Missing semicolon".to_string(),
317    ///     Location::new(42, 5, 10, 1)
318    /// );
319    ///
320    /// let errors = engine.get_compiler_errors();
321    /// assert_eq!(errors.len(), 1);
322    /// ```
323    pub fn get_compiler_errors(&self) -> Vec<CompilerError> {
324        self.diagnostics
325            .iter()
326            .filter(|d| matches!(d.severity, ErrorSeverity::Error))
327            .map(|d| {
328                CompilerError::new(
329                    d.error_code,
330                    d.message.clone(),
331                    d.location.line,
332                    d.location.column,
333                    d.location.position,
334                    Some(d.location.length),
335                )
336            })
337            .collect()
338    }
339
340    /// Checks if any errors have been collected
341    ///
342    /// ### Returns
343    /// `true` if there are any error-level diagnostics, `false` otherwise
344    ///
345    /// ### Example
346    /// ```rust
347    /// use slang_shared::DiagnosticEngine;
348    ///
349    /// let mut engine = DiagnosticEngine::new();
350    /// assert!(!engine.has_errors());
351    ///
352    /// // After emitting an error...
353    /// // assert!(engine.has_errors());
354    /// ```
355    pub fn has_errors(&self) -> bool {
356        self.error_count > 0
357    }
358
359    /// Returns the total number of errors collected
360    ///
361    /// ### Returns
362    /// The count of error-level diagnostics
363    pub fn error_count(&self) -> usize {
364        self.error_count
365    }
366
367    /// Returns the total number of warnings collected
368    ///
369    /// ### Returns
370    /// The count of warning-level diagnostics
371    pub fn warning_count(&self) -> usize {
372        self.warning_count
373    }
374
375    /// Finishes diagnostic collection and returns the result
376    ///
377    /// ### Returns
378    /// `Ok(())` if no errors were collected, otherwise `Err` with all diagnostics
379    pub fn finish(self) -> Result<(), Vec<Diagnostic>> {
380        if self.has_errors() {
381            Err(self.diagnostics)
382        } else {
383            Ok(())
384        }
385    }
386
387    /// Enables or disables error recovery mode
388    ///
389    /// In recovery mode, the compilation pipeline continues processing even after
390    /// encountering errors, allowing multiple errors to be collected in a single pass.
391    ///
392    /// ### Arguments
393    /// * `enabled` - Whether to enable recovery mode
394    pub fn set_recovery_mode(&mut self, enabled: bool) {
395        self.recovery_mode = enabled;
396    }
397
398    /// Checks if error recovery mode is enabled
399    ///
400    /// ### Returns
401    /// `true` if recovery mode is enabled, `false` otherwise
402    pub fn is_recovery_mode(&self) -> bool {
403        self.recovery_mode
404    }
405
406    /// Sets the file name for better error reporting
407    ///
408    /// ### Arguments
409    /// * `file_name` - The name of the file being compiled
410    pub fn set_file_name(&mut self, file_name: String) {
411        self.file_name = Some(file_name);
412    }
413
414    /// Sets the source text for better error reporting
415    ///
416    /// ### Arguments
417    /// * `source_text` - The source code being compiled
418    pub fn set_source_text(&mut self, source_text: &'a str) {
419        self.source_text = Some(source_text);
420    }
421
422    /// Sets the maximum number of errors before stopping compilation
423    ///
424    /// ### Arguments
425    /// * `max_errors` - The maximum number of errors to collect
426    pub fn set_max_errors(&mut self, max_errors: usize) {
427        self.max_errors = max_errors;
428    }
429
430    /// Consumes the engine and returns all collected diagnostics
431    ///
432    /// ### Returns
433    /// A vector containing all diagnostics that were collected
434    pub fn into_errors(self) -> Vec<Diagnostic> {
435        self.diagnostics
436    }
437
438    /// Reports all diagnostics to stderr with rich formatting
439    ///
440    /// This method provides comprehensive error reporting with colored output,
441    /// source code context, line numbers, and helpful suggestions.
442    ///
443    /// ### Arguments
444    /// * `source` - The source code to display in error context
445    ///
446    /// ### Example
447    /// ```rust
448    /// use slang_shared::DiagnosticEngine;
449    ///
450    /// let engine = DiagnosticEngine::new();
451    /// // ... collect some diagnostics ...
452    /// engine.report_all(&source_code);
453    /// ```
454    pub fn report_all(&self, source: &str) {
455        let line_info = LineInfo::new(source);
456        for diagnostic in &self.diagnostics {
457            self.report_diagnostic(diagnostic, &line_info);
458        }
459
460        if self.error_count > 0 || self.warning_count > 0 {
461            self.report_summary();
462        }
463    }
464
465    /// Emits a "too many errors" diagnostic when the error limit is reached
466    ///
467    /// This private method is called automatically when the error count reaches
468    /// the configured maximum, helping to prevent overwhelming output in cases
469    /// of cascading errors.
470    fn emit_too_many_errors(&mut self) {
471        let diagnostic = Diagnostic {
472            severity: ErrorSeverity::Error,
473            error_code: ErrorCode::GenericCompileError,
474            message: format!(
475                "Too many errors ({}), stopping compilation",
476                self.max_errors
477            ),
478            location: Location::new(0, 1, 1, 1),
479            suggestions: Vec::new(),
480            related: Vec::new(),
481        };
482        self.diagnostics.push(diagnostic);
483    }
484
485    /// Formats and prints a single diagnostic with rich formatting
486    ///
487    /// This private method handles the detailed formatting of individual diagnostics,
488    /// including colored output, source code context, line markers, and suggestions.
489    ///
490    /// ### Arguments
491    /// * `diagnostic` - The diagnostic to format and display
492    /// * `line_info` - Line information for displaying source context
493    fn report_diagnostic(&self, diagnostic: &Diagnostic, line_info: &LineInfo) {
494        let severity_str = match diagnostic.severity {
495            ErrorSeverity::Error => "error".red().bold(),
496            ErrorSeverity::Warning => "warning".yellow().bold(),
497            ErrorSeverity::Note => "note".blue().bold(),
498        };
499
500        let line = diagnostic.location.line;
501        let col = diagnostic.location.column;
502        let current_line_text = line_info
503            .get_line_text(line)
504            .unwrap_or("<line not available>");
505
506        eprintln!(
507            "{} {}: {}",
508            severity_str,
509            diagnostic.error_code.to_string().bold(),
510            diagnostic.message
511        );
512
513        eprintln!("  {} {}:{}:{}", "-->".yellow(), "main", line, col);
514
515        let line_num_str = format!("{}", line);
516        let indent_width = line_num_str.len() + 1;
517        let indent = " ".repeat(indent_width);
518        let pipe = "|".yellow();
519
520        eprintln!("{indent}{}", pipe);
521        eprintln!("{} {} {}", line_num_str.yellow(), pipe, current_line_text);
522
523        let error_marker = " ".repeat(col.saturating_sub(1))
524            + &"^"
525                .repeat(diagnostic.location.length.max(1))
526                .bold()
527                .red()
528                .to_string();
529        eprintln!("{indent}{} {}", pipe, error_marker);
530
531        for suggestion in &diagnostic.suggestions {
532            eprintln!(
533                "{indent}{} {}: {}",
534                pipe,
535                "help".green().bold(),
536                suggestion.message
537            );
538        }
539
540        eprintln!();
541    }
542
543    /// Prints a summary of all collected diagnostics
544    ///
545    /// This private method displays a final summary showing the total count
546    /// of errors and warnings encountered during compilation.
547    fn report_summary(&self) {
548        let mut parts = Vec::new();
549
550        if self.error_count > 0 {
551            parts.push(
552                format!(
553                    "{} {}",
554                    self.error_count,
555                    if self.error_count == 1 {
556                        "error"
557                    } else {
558                        "errors"
559                    }
560                )
561                .red()
562                .to_string(),
563            );
564        }
565
566        if self.warning_count > 0 {
567            parts.push(
568                format!(
569                    "{} {}",
570                    self.warning_count,
571                    if self.warning_count == 1 {
572                        "warning"
573                    } else {
574                        "warnings"
575                    }
576                )
577                .yellow()
578                .to_string(),
579            );
580        }
581
582        if !parts.is_empty() {
583            eprintln!("Compilation finished with {}", parts.join(", "));
584        }
585    }
586
587    /// Removes and returns all collected diagnostics, resetting counters
588    ///
589    /// This method provides a way to extract all diagnostics while clearing
590    /// the internal state. Useful for batch processing or transferring
591    /// diagnostics to another system.
592    ///
593    /// ### Returns
594    /// A vector containing all previously collected diagnostics
595    pub fn take_diagnostics(&mut self) -> Vec<Diagnostic> {
596        let diagnostics = std::mem::take(&mut self.diagnostics);
597        self.error_count = 0;
598        self.warning_count = 0;
599        diagnostics
600    }
601}
602
603impl Default for DiagnosticEngine<'_> {
604    fn default() -> Self {
605        Self::new()
606    }
607}