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}