mos_core/sink.rs
1//! Diagnostic emission plumbing.
2//!
3//! Compiler phases push diagnostics into a [`DiagnosticSink`] rather than
4//! returning a `Vec`. This crate ships exactly one sink, [`CollectingSink`],
5//! which gathers everything and tracks whether any error-severity
6//! diagnostic was seen. Rendering sinks (and any future
7//! suppression/severity-resolver wrappers) live in the consumer — the CLI
8//! binary owns presentation, `mos-core` owns data.
9//!
10//! ## `Err` means *structural abort*, not *error diagnostic*
11//!
12//! [`DiagnosticResult::Err`] signals that a phase cannot structurally
13//! continue producing meaningful output (a fatal IO failure, a violated
14//! internal invariant). It is **not** returned merely because an
15//! `Error`-severity diagnostic was emitted. Ordinary error diagnostics are
16//! collected and remembered via [`CollectingSink::had_error`]; the caller
17//! (the CLI) enforces phase barriers by checking that between phases and
18//! exiting before the next one starts. Conflating the two would unwind the
19//! parser on the first error and hide every later one.
20
21use crate::Diagnostic;
22use crate::Severity;
23
24/// A phase aborted for structural reasons (see the module docs). Carries
25/// no payload: the diagnostics explaining *why* have already been emitted
26/// to the sink.
27#[derive(Copy, Clone, Eq, PartialEq, Debug)]
28pub struct DiagnosticAbort;
29
30/// Result of a compiler phase. `Ok(T)` on (possibly diagnostic-bearing)
31/// completion; `Err(DiagnosticAbort)` only on a structural abort.
32pub type DiagnosticResult<T> = Result<T, DiagnosticAbort>;
33
34/// Receiver for diagnostics emitted during a compiler phase.
35///
36/// Implementors decide what to do with each [`Diagnostic`] (collect,
37/// render, count). Returning `Err(DiagnosticAbort)` asks the phase to stop
38/// for structural reasons; the in-tree sinks never do, so `?` on an
39/// `emit` is a no-op in practice and the phase runs to completion.
40pub trait DiagnosticSink {
41 /// Accept one fully-built diagnostic.
42 ///
43 /// # Errors
44 ///
45 /// Returns [`DiagnosticAbort`] only if the sink wants the current
46 /// phase to stop for structural reasons (not implemented by the
47 /// in-tree sinks).
48 fn emit(&mut self, diagnostic: Diagnostic) -> DiagnosticResult<()>;
49}
50
51/// Collects every diagnostic and remembers whether any was an error.
52///
53/// Used by tests and by library callers that want the full list. Always
54/// returns `Ok(())` from [`emit`](DiagnosticSink::emit).
55///
56/// # Examples
57///
58/// ```
59/// use mos_core::{CollectingSink, Diagnostic, DiagnosticSink, Severity, codes};
60///
61/// let mut sink = CollectingSink::new();
62/// assert!(
63/// sink.emit(Diagnostic::simple(&codes::MOS0028, None, "unterminated"))
64/// .is_ok(),
65/// "collecting sink must not abort"
66/// );
67/// assert!(
68/// sink.emit(Diagnostic::simple(&codes::MOS0010, None, "missing ident"))
69/// .is_ok(),
70/// "collecting sink must not abort"
71/// );
72///
73/// assert!(sink.had_error()); // MOS0010 is an error
74/// assert_eq!(sink.into_diagnostics().len(), 2);
75/// ```
76#[derive(Default, Debug)]
77pub struct CollectingSink {
78 diagnostics: Vec<Diagnostic>,
79 had_error: bool,
80}
81
82impl CollectingSink {
83 /// A fresh, empty sink.
84 #[must_use]
85 pub fn new() -> Self {
86 Self::default()
87 }
88
89 /// Whether any `Error`-severity diagnostic has been emitted so far.
90 #[must_use]
91 pub fn had_error(&self) -> bool {
92 self.had_error
93 }
94
95 /// Borrow the collected diagnostics in emission order.
96 #[must_use]
97 pub fn diagnostics(&self) -> &[Diagnostic] {
98 &self.diagnostics
99 }
100
101 /// Consume the sink, yielding the collected diagnostics.
102 #[must_use]
103 pub fn into_diagnostics(self) -> Vec<Diagnostic> {
104 self.diagnostics
105 }
106}
107
108impl DiagnosticSink for CollectingSink {
109 fn emit(&mut self, diagnostic: Diagnostic) -> DiagnosticResult<()> {
110 if diagnostic.severity() == Severity::Error {
111 self.had_error = true;
112 }
113 self.diagnostics.push(diagnostic);
114 Ok(())
115 }
116}
117
118/// Forwarding impl so phases can take `&mut dyn DiagnosticSink` and callers
119/// can pass `&mut sink` of a concrete type through several layers.
120impl<S: DiagnosticSink + ?Sized> DiagnosticSink for &mut S {
121 fn emit(&mut self, diagnostic: Diagnostic) -> DiagnosticResult<()> {
122 (**self).emit(diagnostic)
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::codes;
130
131 #[test]
132 fn collecting_sink_tracks_error_flag_and_order() {
133 let mut sink = CollectingSink::new();
134 assert!(!sink.had_error());
135
136 assert!(
137 sink.emit(Diagnostic::simple(&codes::MOS0018, None, "notice"))
138 .is_ok(),
139 "collecting sink must not abort"
140 );
141 assert!(!sink.had_error(), "a notice must not flip had_error");
142
143 assert!(
144 sink.emit(Diagnostic::simple(&codes::MOS0028, None, "warning"))
145 .is_ok(),
146 "collecting sink must not abort"
147 );
148 assert!(!sink.had_error(), "a warning must not flip had_error");
149
150 assert!(
151 sink.emit(Diagnostic::simple(&codes::MOS0010, None, "error"))
152 .is_ok(),
153 "collecting sink must not abort"
154 );
155 assert!(sink.had_error(), "an error must flip had_error");
156
157 let diags = sink.into_diagnostics();
158 assert_eq!(diags.len(), 3);
159 assert_eq!(diags[0].def().code(), codes::MOS0018.code());
160 assert_eq!(diags[2].severity(), Severity::Error);
161 }
162}