midenc_session/
diagnostics.rs

1#![expect(unused_assignments)]
2
3use alloc::{
4    boxed::Box,
5    collections::BTreeMap,
6    fmt::{self, Display},
7    format,
8    string::{String, ToString},
9    sync::Arc,
10    vec::Vec,
11};
12use core::sync::atomic::{AtomicUsize, Ordering};
13
14pub use miden_assembly::diagnostics::{
15    Diagnostic, Label, LabeledSpan, RelatedError, RelatedLabel, Report, Severity, WrapErr, miette,
16    miette::MietteDiagnostic as AdHocDiagnostic,
17    reporting,
18    reporting::{PrintDiagnostic, ReportHandlerOpts},
19};
20pub use miden_core::*;
21pub use miden_debug_types::*;
22pub use midenc_hir_macros::Spanned;
23
24#[cfg(feature = "std")]
25pub use crate::emitter::CaptureEmitter;
26pub use crate::emitter::{Buffer, DefaultEmitter, Emitter, NullEmitter};
27use crate::{ColorChoice, Verbosity, Warnings};
28
29#[derive(Default, Debug, Copy, Clone)]
30pub struct DiagnosticsConfig {
31    pub verbosity: Verbosity,
32    pub warnings: Warnings,
33}
34
35pub struct DiagnosticsHandler {
36    emitter: Arc<dyn Emitter>,
37    source_manager: Arc<dyn SourceManager + Send + Sync>,
38    err_count: AtomicUsize,
39    verbosity: Verbosity,
40    warnings: Warnings,
41    silent: bool,
42}
43
44impl Default for DiagnosticsHandler {
45    fn default() -> Self {
46        let emitter = Arc::new(DefaultEmitter::new(ColorChoice::Auto));
47        let source_manager =
48            Arc::new(DefaultSourceManager::default()) as Arc<dyn SourceManager + Send + Sync>;
49        Self::new(Default::default(), source_manager, emitter)
50    }
51}
52
53// We can safely implement these traits for DiagnosticsHandler,
54// as the only two non-atomic fields are read-only after creation
55unsafe impl Send for DiagnosticsHandler {}
56unsafe impl Sync for DiagnosticsHandler {}
57
58impl DiagnosticsHandler {
59    /// Create a new [DiagnosticsHandler] from the given [DiagnosticsConfig],
60    /// [CodeMap], and [Emitter] implementation.
61    pub fn new(
62        config: DiagnosticsConfig,
63        source_manager: Arc<dyn SourceManager + Send + Sync>,
64        emitter: Arc<dyn Emitter>,
65    ) -> Self {
66        let warnings = match config.warnings {
67            Warnings::Error => Warnings::Error,
68            _ if config.verbosity > Verbosity::Warning => Warnings::None,
69            warnings => warnings,
70        };
71        Self {
72            emitter,
73            source_manager,
74            err_count: AtomicUsize::new(0),
75            verbosity: config.verbosity,
76            warnings,
77            silent: config.verbosity == Verbosity::Silent,
78        }
79    }
80
81    #[inline]
82    pub fn source_manager(&self) -> Arc<dyn SourceManager + Send + Sync> {
83        self.source_manager.clone()
84    }
85
86    #[inline]
87    pub fn source_manager_ref(&self) -> &dyn SourceManager {
88        self.source_manager.as_ref()
89    }
90
91    /// Returns true if the [DiagnosticsHandler] has emitted any error diagnostics
92    pub fn has_errors(&self) -> bool {
93        self.err_count.load(Ordering::Relaxed) > 0
94    }
95
96    /// Triggers a panic if the [DiagnosticsHandler] has emitted any error diagnostics
97    #[track_caller]
98    pub fn abort_if_errors(&self) {
99        if self.has_errors() {
100            panic!("Compiler has encountered unexpected errors. See diagnostics for details.")
101        }
102    }
103
104    /// Emit a diagnostic [Report]
105    pub fn report(&self, report: impl Into<Report>) {
106        self.emit(report.into())
107    }
108
109    /// Report an error diagnostic
110    pub fn error(&self, error: impl ToString) {
111        self.emit(Report::msg(error.to_string()));
112    }
113
114    /// Report a warning diagnostic
115    ///
116    /// If `warnings_as_errors` is set, it produces an error diagnostic instead.
117    pub fn warn(&self, warning: impl ToString) {
118        if matches!(self.warnings, Warnings::Error) {
119            return self.error(warning);
120        }
121        let diagnostic = AdHocDiagnostic::new(warning.to_string()).with_severity(Severity::Warning);
122        self.emit(diagnostic);
123    }
124
125    /// Emits an informational diagnostic
126    pub fn info(&self, message: impl ToString) {
127        if self.verbosity > Verbosity::Info {
128            return;
129        }
130        let diagnostic = AdHocDiagnostic::new(message.to_string()).with_severity(Severity::Advice);
131        self.emit(diagnostic);
132    }
133
134    /// Starts building an [InFlightDiagnostic] for rich compiler diagnostics.
135    ///
136    /// The caller is responsible for dropping/emitting the diagnostic using the
137    /// [InFlightDiagnostic] API.
138    pub fn diagnostic(&self, severity: Severity) -> InFlightDiagnosticBuilder<'_> {
139        InFlightDiagnosticBuilder::new(self, severity)
140    }
141
142    /// Emits the given diagnostic
143    #[inline(never)]
144    pub fn emit(&self, diagnostic: impl Into<Report>) {
145        let diagnostic: Report = diagnostic.into();
146        let diagnostic = match diagnostic.severity() {
147            Some(Severity::Advice) if self.verbosity > Verbosity::Info => return,
148            Some(Severity::Warning) => match self.warnings {
149                Warnings::None => return,
150                Warnings::All => diagnostic,
151                Warnings::Error => {
152                    self.err_count.fetch_add(1, Ordering::Relaxed);
153                    Report::from(WarningAsError::from(diagnostic))
154                }
155            },
156            Some(Severity::Error) => {
157                self.err_count.fetch_add(1, Ordering::Relaxed);
158                diagnostic
159            }
160            _ => diagnostic,
161        };
162
163        if self.silent {
164            return;
165        }
166
167        self.write_report(diagnostic);
168    }
169
170    #[cfg(feature = "std")]
171    fn write_report(&self, diagnostic: Report) {
172        use std::io::Write;
173
174        let mut buffer = self.emitter.buffer();
175        let printer = PrintDiagnostic::new(diagnostic);
176        write!(&mut buffer, "{printer}").expect("failed to write diagnostic to buffer");
177        self.emitter.print(buffer).unwrap();
178    }
179
180    #[cfg(not(feature = "std"))]
181    fn write_report(&self, diagnostic: Report) {
182        use core::fmt::Write;
183
184        let mut buffer = self.emitter.buffer();
185        let printer = PrintDiagnostic::new(diagnostic);
186        write!(&mut buffer, "{printer}").expect("failed to write diagnostic to buffer");
187        self.emitter.print(buffer).unwrap();
188    }
189}
190
191#[derive(thiserror::Error, Diagnostic, Debug)]
192#[error("{}", .report)]
193#[diagnostic(
194    severity(Error),
195    help("this warning was promoted to an error via --warnings-as-errors")
196)]
197struct WarningAsError {
198    #[diagnostic_source]
199    report: Report,
200}
201impl From<Report> for WarningAsError {
202    fn from(report: Report) -> Self {
203        Self { report }
204    }
205}
206
207/// Constructs an in-flight diagnostic using the builder pattern
208pub struct InFlightDiagnosticBuilder<'h> {
209    handler: &'h DiagnosticsHandler,
210    diagnostic: InFlightDiagnostic,
211    /// The source id of the primary diagnostic being constructed, if known
212    primary_source_id: Option<SourceId>,
213    /// The set of secondary labels which reference code in other source files than the primary
214    references: BTreeMap<SourceId, RelatedLabel>,
215}
216impl<'h> InFlightDiagnosticBuilder<'h> {
217    pub(crate) fn new(handler: &'h DiagnosticsHandler, severity: Severity) -> Self {
218        Self {
219            handler,
220            diagnostic: InFlightDiagnostic::new(severity),
221            primary_source_id: None,
222            references: BTreeMap::default(),
223        }
224    }
225
226    /// Sets the primary diagnostic message to `message`
227    pub fn with_message(mut self, message: impl ToString) -> Self {
228        self.diagnostic.message = message.to_string();
229        self
230    }
231
232    /// Sets the error code for this diagnostic
233    pub fn with_code(mut self, code: impl ToString) -> Self {
234        self.diagnostic.code = Some(code.to_string());
235        self
236    }
237
238    /// Sets the error url for this diagnostic
239    pub fn with_url(mut self, url: impl ToString) -> Self {
240        self.diagnostic.url = Some(url.to_string());
241        self
242    }
243
244    /// Adds a primary label for `span` to this diagnostic, with no label message.
245    pub fn with_primary_span(mut self, span: SourceSpan) -> Self {
246        use miden_assembly::diagnostics::LabeledSpan;
247
248        assert!(self.diagnostic.labels.is_empty(), "cannot set the primary span more than once");
249        let source_id = span.source_id();
250        let source_file = self.handler.source_manager.get(source_id).ok();
251        self.primary_source_id = Some(source_id);
252        self.diagnostic.source_code = source_file;
253        self.diagnostic.labels.push(LabeledSpan::new_primary_with_span(None, span));
254        self
255    }
256
257    /// Adds a primary label for `span` to this diagnostic, with the given message
258    ///
259    /// A primary label is one which should be rendered as the relevant source code
260    /// at which a diagnostic originates. Secondary labels are used for related items
261    /// involved in the diagnostic.
262    pub fn with_primary_label(mut self, span: SourceSpan, message: impl ToString) -> Self {
263        use miden_assembly::diagnostics::LabeledSpan;
264
265        assert!(self.diagnostic.labels.is_empty(), "cannot set the primary span more than once");
266        let source_id = span.source_id();
267        let source_file = self.handler.source_manager.get(source_id).ok();
268        self.primary_source_id = Some(source_id);
269        self.diagnostic.source_code = source_file;
270        self.diagnostic
271            .labels
272            .push(LabeledSpan::new_primary_with_span(Some(message.to_string()), span));
273        self
274    }
275
276    /// Adds a secondary label for `span` to this diagnostic, with the given message
277    ///
278    /// A secondary label is used to point out related items in the source code which
279    /// are relevant to the diagnostic, but which are not themselves the point at which
280    /// the diagnostic originates.
281    pub fn with_secondary_label(mut self, span: SourceSpan, message: impl ToString) -> Self {
282        use miden_assembly::diagnostics::LabeledSpan;
283
284        assert!(
285            !self.diagnostic.labels.is_empty(),
286            "must set a primary label before any secondary labels"
287        );
288        let source_id = span.source_id();
289        if source_id != self.primary_source_id.unwrap_or_default() {
290            let related = self.references.entry(source_id).or_insert_with(|| {
291                let source_file = self.handler.source_manager.get(source_id).ok();
292                RelatedLabel::advice("see diagnostics for more information")
293                    .with_source_file(source_file)
294            });
295            related.labels.push(Label::new(span, message.to_string()));
296        } else {
297            self.diagnostic
298                .labels
299                .push(LabeledSpan::new_with_span(Some(message.to_string()), span));
300        }
301        self
302    }
303
304    /// Adds a note to the diagnostic
305    ///
306    /// Notes are used for explaining general concepts or suggestions
307    /// related to a diagnostic, and are not associated with any particular
308    /// source location. They are always rendered after the other diagnostic
309    /// content.
310    pub fn with_help(mut self, note: impl ToString) -> Self {
311        self.diagnostic.help = Some(note.to_string());
312        self
313    }
314
315    /// Consume this [InFlightDiagnostic] and create a [Report]
316    pub fn into_report(mut self) -> Report {
317        if self.diagnostic.message.is_empty() {
318            self.diagnostic.message = "reported".into();
319        }
320        self.diagnostic.related.extend(self.references.into_values());
321        Report::from(self.diagnostic)
322    }
323
324    /// Emit the underlying [Diagnostic] via the [DiagnosticHandler]
325    pub fn emit(self) {
326        let handler = self.handler;
327        handler.emit(self.into_report());
328    }
329}
330
331#[derive(Default)]
332struct InFlightDiagnostic {
333    source_code: Option<Arc<SourceFile>>,
334    severity: Option<Severity>,
335    message: String,
336    code: Option<String>,
337    help: Option<String>,
338    url: Option<String>,
339    labels: Vec<LabeledSpan>,
340    related: Vec<RelatedLabel>,
341}
342
343impl InFlightDiagnostic {
344    fn new(severity: Severity) -> Self {
345        Self {
346            severity: Some(severity),
347            ..Default::default()
348        }
349    }
350}
351
352impl fmt::Display for InFlightDiagnostic {
353    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
354        write!(f, "{}", &self.message)
355    }
356}
357
358impl fmt::Debug for InFlightDiagnostic {
359    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
360        write!(f, "{}", &self.message)
361    }
362}
363
364impl core::error::Error for InFlightDiagnostic {}
365
366impl Diagnostic for InFlightDiagnostic {
367    fn code<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
368        self.code.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
369    }
370
371    fn severity(&self) -> Option<Severity> {
372        self.severity
373    }
374
375    fn help<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
376        self.help.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
377    }
378
379    fn url<'a>(&'a self) -> Option<Box<dyn Display + 'a>> {
380        self.url.as_ref().map(Box::new).map(|c| c as Box<dyn Display>)
381    }
382
383    fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
384        if self.labels.is_empty() {
385            return None;
386        }
387        let iter = self.labels.iter().cloned();
388        Some(Box::new(iter) as Box<dyn Iterator<Item = LabeledSpan>>)
389    }
390
391    fn related(&self) -> Option<Box<dyn Iterator<Item = &dyn Diagnostic> + '_>> {
392        if self.related.is_empty() {
393            return None;
394        }
395
396        let iter = self.related.iter().map(|r| r as &dyn Diagnostic);
397        Some(Box::new(iter) as Box<dyn Iterator<Item = &dyn Diagnostic>>)
398    }
399
400    fn diagnostic_source(&self) -> Option<&(dyn Diagnostic + '_)> {
401        None
402    }
403}
404
405pub use self::into_diagnostic::{DiagnosticError, IntoDiagnostic};
406
407mod into_diagnostic {
408    use alloc::boxed::Box;
409
410    /// Convenience [`Diagnostic`] that can be used as an "anonymous" wrapper for
411    /// Errors. This is intended to be paired with [`IntoDiagnostic`].
412    #[derive(Debug)]
413    pub struct DiagnosticError<E>(Box<E>);
414    impl<E> DiagnosticError<E> {
415        pub fn new(error: E) -> Self {
416            Self(Box::new(error))
417        }
418    }
419    impl<E: core::fmt::Debug + core::fmt::Display + 'static> miden_assembly::diagnostics::Diagnostic
420        for DiagnosticError<E>
421    {
422    }
423    impl<E: core::fmt::Display> core::fmt::Display for DiagnosticError<E> {
424        fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
425            core::fmt::Display::fmt(self.0.as_ref(), f)
426        }
427    }
428    impl<E: core::fmt::Debug + core::fmt::Display + 'static> core::error::Error for DiagnosticError<E> {
429        default fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
430            None
431        }
432
433        default fn cause(&self) -> Option<&dyn core::error::Error> {
434            self.source()
435        }
436    }
437    impl<E: core::error::Error + 'static> core::error::Error for DiagnosticError<E> {
438        fn source(&self) -> Option<&(dyn core::error::Error + 'static)> {
439            self.0.source()
440        }
441    }
442    unsafe impl<E: Send> Send for DiagnosticError<E> {}
443    unsafe impl<E: Sync> Sync for DiagnosticError<E> {}
444
445    /**
446    Convenience trait that adds a [`.into_diagnostic()`](IntoDiagnostic::into_diagnostic) method that converts a type implementing
447    [`std::error::Error`] to a [`Result<T, Report>`].
448
449    ## Warning
450
451    Calling this on a type implementing [`Diagnostic`] will reduce it to the common denominator of
452    [`std::error::Error`]. Meaning all extra information provided by [`Diagnostic`] will be
453    inaccessible. If you have a type implementing [`Diagnostic`] consider simply returning it or using
454    [`Into`] or the [`Try`](std::ops::Try) operator (`?`).
455    */
456    pub trait IntoDiagnostic<T, E> {
457        /// Converts [`Result`] types that return regular [`std::error::Error`]s
458        /// into a [`Result`] that returns a [`Diagnostic`].
459        fn into_diagnostic(self) -> Result<T, super::Report>;
460    }
461
462    impl<T, E: core::fmt::Debug + core::fmt::Display + Sync + Send + 'static> IntoDiagnostic<T, E>
463        for Result<T, E>
464    {
465        fn into_diagnostic(self) -> Result<T, super::Report> {
466            self.map_err(|e| DiagnosticError::new(e).into())
467        }
468    }
469}