Skip to main content

tensorlogic_compiler/error_recovery/
collector.rs

1//! Thread-safe diagnostic collector used by the tolerant compiler.
2//!
3//! The collector is a shareable sink for [`Diagnostic`] values produced during
4//! tolerant compilation. It stores diagnostics in **insertion order** and
5//! allows:
6//!
7//! * concurrent push from multiple worker threads via [`Arc<Mutex<_>>`];
8//! * querying by severity (`errors()`, `warnings()`, `fatals()`, ...);
9//! * filtering by expression index (`for_expression()`);
10//! * clearing and replacing the buffer wholesale (for test harnesses).
11//!
12//! # Example
13//!
14//! ```
15//! use tensorlogic_compiler::error_recovery::{
16//!     Diagnostic, DiagnosticCollector, Severity,
17//! };
18//!
19//! let c = DiagnosticCollector::new();
20//! c.push(Diagnostic::warning("deprecated"));
21//! c.push(Diagnostic::error("arity mismatch").with_expression_index(1));
22//!
23//! assert_eq!(c.len(), 2);
24//! assert_eq!(c.count_of(Severity::Error), 1);
25//! assert!(c.has_blocking());
26//! ```
27
28use std::sync::{Arc, Mutex};
29
30use super::diagnostic::{Diagnostic, Severity};
31
32/// Shared, thread-safe collector for [`Diagnostic`] values.
33///
34/// Cloning a `DiagnosticCollector` yields another handle to the *same*
35/// underlying buffer (cheap `Arc` clone) — this is intentional: it allows
36/// passing the collector into worker tasks without ceremony.
37#[derive(Debug, Clone, Default)]
38pub struct DiagnosticCollector {
39    inner: Arc<Mutex<Vec<Diagnostic>>>,
40}
41
42impl DiagnosticCollector {
43    /// Construct an empty collector.
44    pub fn new() -> Self {
45        Self::default()
46    }
47
48    /// Construct a collector preseeded with the given diagnostics.
49    pub fn with_diagnostics(initial: Vec<Diagnostic>) -> Self {
50        Self {
51            inner: Arc::new(Mutex::new(initial)),
52        }
53    }
54
55    /// Push a single diagnostic. On a poisoned mutex the diagnostic is still
56    /// pushed using the recovered guard — partial progress is always
57    /// preserved.
58    pub fn push(&self, diag: Diagnostic) {
59        let mut guard = match self.inner.lock() {
60            Ok(g) => g,
61            Err(poisoned) => poisoned.into_inner(),
62        };
63        guard.push(diag);
64    }
65
66    /// Push multiple diagnostics in one shot, preserving their order.
67    pub fn extend<I: IntoIterator<Item = Diagnostic>>(&self, diags: I) {
68        let mut guard = match self.inner.lock() {
69            Ok(g) => g,
70            Err(poisoned) => poisoned.into_inner(),
71        };
72        guard.extend(diags);
73    }
74
75    /// Total number of diagnostics collected so far.
76    pub fn len(&self) -> usize {
77        self.snapshot_raw().len()
78    }
79
80    /// Returns `true` when no diagnostics have been collected.
81    pub fn is_empty(&self) -> bool {
82        self.len() == 0
83    }
84
85    /// Clone out the full diagnostic list in insertion order.
86    pub fn snapshot(&self) -> Vec<Diagnostic> {
87        self.snapshot_raw()
88    }
89
90    fn snapshot_raw(&self) -> Vec<Diagnostic> {
91        match self.inner.lock() {
92            Ok(g) => g.clone(),
93            Err(poisoned) => poisoned.into_inner().clone(),
94        }
95    }
96
97    /// All diagnostics of the given severity, in insertion order.
98    pub fn of_severity(&self, severity: Severity) -> Vec<Diagnostic> {
99        self.snapshot_raw()
100            .into_iter()
101            .filter(|d| d.severity == severity)
102            .collect()
103    }
104
105    /// All diagnostics whose severity is at least `min` (inclusive), in
106    /// insertion order.
107    pub fn at_least(&self, min: Severity) -> Vec<Diagnostic> {
108        self.snapshot_raw()
109            .into_iter()
110            .filter(|d| d.severity >= min)
111            .collect()
112    }
113
114    /// Count of diagnostics of a specific severity.
115    pub fn count_of(&self, severity: Severity) -> usize {
116        self.snapshot_raw()
117            .iter()
118            .filter(|d| d.severity == severity)
119            .count()
120    }
121
122    /// Convenience accessor: all [`Severity::Error`] diagnostics.
123    pub fn errors(&self) -> Vec<Diagnostic> {
124        self.of_severity(Severity::Error)
125    }
126
127    /// Convenience accessor: all [`Severity::Warning`] diagnostics.
128    pub fn warnings(&self) -> Vec<Diagnostic> {
129        self.of_severity(Severity::Warning)
130    }
131
132    /// Convenience accessor: all [`Severity::Fatal`] diagnostics.
133    pub fn fatals(&self) -> Vec<Diagnostic> {
134        self.of_severity(Severity::Fatal)
135    }
136
137    /// Convenience accessor: all [`Severity::Info`] diagnostics.
138    pub fn infos(&self) -> Vec<Diagnostic> {
139        self.of_severity(Severity::Info)
140    }
141
142    /// Returns `true` iff at least one diagnostic is blocking
143    /// (Error or Fatal).
144    pub fn has_blocking(&self) -> bool {
145        self.snapshot_raw().iter().any(Diagnostic::is_blocking)
146    }
147
148    /// Returns `true` iff at least one Fatal diagnostic exists.
149    pub fn has_fatal(&self) -> bool {
150        self.snapshot_raw().iter().any(Diagnostic::is_fatal)
151    }
152
153    /// All diagnostics attached to the given expression index, in insertion
154    /// order.
155    pub fn for_expression(&self, idx: usize) -> Vec<Diagnostic> {
156        self.snapshot_raw()
157            .into_iter()
158            .filter(|d| d.expression_index == Some(idx))
159            .collect()
160    }
161
162    /// Drain the buffer, returning all diagnostics and leaving the collector
163    /// empty.
164    pub fn drain(&self) -> Vec<Diagnostic> {
165        let mut guard = match self.inner.lock() {
166            Ok(g) => g,
167            Err(poisoned) => poisoned.into_inner(),
168        };
169        std::mem::take(&mut *guard)
170    }
171
172    /// Reset the collector to empty.
173    pub fn clear(&self) {
174        let _ = self.drain();
175    }
176}
177
178#[cfg(test)]
179mod tests {
180    use super::super::diagnostic::{Diagnostic, Severity};
181    use super::*;
182
183    #[test]
184    fn push_preserves_insertion_order() {
185        let c = DiagnosticCollector::new();
186        c.push(Diagnostic::info("a"));
187        c.push(Diagnostic::warning("b"));
188        c.push(Diagnostic::error("c"));
189        let snap = c.snapshot();
190        assert_eq!(snap.len(), 3);
191        assert_eq!(snap[0].message, "a");
192        assert_eq!(snap[1].message, "b");
193        assert_eq!(snap[2].message, "c");
194    }
195
196    #[test]
197    fn severity_filters() {
198        let c = DiagnosticCollector::new();
199        c.push(Diagnostic::info("i"));
200        c.push(Diagnostic::warning("w1"));
201        c.push(Diagnostic::warning("w2"));
202        c.push(Diagnostic::error("e"));
203        c.push(Diagnostic::fatal("f"));
204
205        assert_eq!(c.len(), 5);
206        assert_eq!(c.count_of(Severity::Warning), 2);
207        assert_eq!(c.errors().len(), 1);
208        assert_eq!(c.warnings().len(), 2);
209        assert_eq!(c.fatals().len(), 1);
210        assert_eq!(c.infos().len(), 1);
211        assert!(c.has_blocking());
212        assert!(c.has_fatal());
213        assert_eq!(c.at_least(Severity::Error).len(), 2);
214    }
215
216    #[test]
217    fn for_expression_filter() {
218        let c = DiagnosticCollector::new();
219        c.push(Diagnostic::error("e0").with_expression_index(0));
220        c.push(Diagnostic::error("e1").with_expression_index(1));
221        c.push(Diagnostic::warning("w1").with_expression_index(1));
222        c.push(Diagnostic::info("no-idx"));
223
224        assert_eq!(c.for_expression(0).len(), 1);
225        assert_eq!(c.for_expression(1).len(), 2);
226        assert_eq!(c.for_expression(99).len(), 0);
227    }
228
229    #[test]
230    fn clone_shares_underlying_buffer() {
231        let c1 = DiagnosticCollector::new();
232        let c2 = c1.clone();
233        c1.push(Diagnostic::error("shared"));
234        assert_eq!(c2.len(), 1);
235    }
236
237    #[test]
238    fn drain_empties_collector() {
239        let c = DiagnosticCollector::with_diagnostics(vec![
240            Diagnostic::error("a"),
241            Diagnostic::warning("b"),
242        ]);
243        assert_eq!(c.len(), 2);
244        let drained = c.drain();
245        assert_eq!(drained.len(), 2);
246        assert!(c.is_empty());
247    }
248
249    #[test]
250    fn extend_appends_multiple() {
251        let c = DiagnosticCollector::new();
252        c.extend(vec![
253            Diagnostic::info("x"),
254            Diagnostic::warning("y"),
255            Diagnostic::error("z"),
256        ]);
257        assert_eq!(c.len(), 3);
258        assert_eq!(c.snapshot()[2].message, "z");
259    }
260}