Skip to main content

tensorlogic_compiler/error_recovery/
diagnostic.rs

1//! Diagnostic primitives used by the tolerant compiler.
2//!
3//! A [`Diagnostic`] is a single, self-contained report describing a compilation
4//! issue. Each diagnostic carries a [`Severity`], a human-readable message, an
5//! optional source span, and an optional *expression index* — i.e. the index
6//! of the offending expression inside the slice passed to
7//! [`crate::error_recovery::TolerantCompiler::compile_program`].
8//!
9//! # Severity semantics
10//!
11//! * [`Severity::Fatal`] — Unrecoverable failure. Under the default
12//!   [`crate::error_recovery::RecoveryStrategy::SkipOnFatal`] strategy the
13//!   remaining expressions are NOT compiled.
14//! * [`Severity::Error`] — The current expression is dropped (its slot becomes
15//!   `None`) but compilation of the rest of the program continues.
16//! * [`Severity::Warning`] — The current expression still compiles; the
17//!   warning is merely recorded.
18//! * [`Severity::Info`] — Informational message; compilation proceeds.
19//!
20//! # Example
21//!
22//! ```
23//! use tensorlogic_compiler::error_recovery::{Diagnostic, Severity};
24//!
25//! let d = Diagnostic::error("arity mismatch").with_expression_index(2);
26//! assert!(d.is_blocking());
27//! assert_eq!(d.severity, Severity::Error);
28//! ```
29
30use std::fmt;
31
32/// Lightweight span into a source program — a byte range `[start, end)` plus an
33/// optional filename or logical label.
34///
35/// This type deliberately does not depend on any particular parser — it is a
36/// minimal, self-contained substitute that can be produced by any upstream
37/// frontend. The compiler never dereferences the span; it is only propagated
38/// for diagnostic printing.
39#[derive(Debug, Clone, PartialEq, Eq, Hash)]
40pub struct SourceSpan {
41    /// Byte offset of the first character (inclusive).
42    pub start: usize,
43    /// Byte offset one past the last character (exclusive).
44    pub end: usize,
45    /// Logical source label (e.g. a file path or `"<stdin>"`); optional.
46    pub source: Option<String>,
47}
48
49impl SourceSpan {
50    /// Construct a span without a source label.
51    pub fn new(start: usize, end: usize) -> Self {
52        Self {
53            start,
54            end,
55            source: None,
56        }
57    }
58
59    /// Construct a span carrying a source label (e.g. a filename).
60    pub fn with_source(start: usize, end: usize, source: impl Into<String>) -> Self {
61        Self {
62            start,
63            end,
64            source: Some(source.into()),
65        }
66    }
67
68    /// Length of the span in bytes (`end − start`, saturating to zero).
69    pub fn len(&self) -> usize {
70        self.end.saturating_sub(self.start)
71    }
72
73    /// Returns `true` when the span is empty (length zero).
74    pub fn is_empty(&self) -> bool {
75        self.end <= self.start
76    }
77}
78
79impl fmt::Display for SourceSpan {
80    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
81        match &self.source {
82            Some(src) => write!(f, "{}:{}..{}", src, self.start, self.end),
83            None => write!(f, "{}..{}", self.start, self.end),
84        }
85    }
86}
87
88/// Diagnostic severity, ordered from least to most serious.
89#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
90pub enum Severity {
91    /// Informational; does not affect compilation.
92    Info,
93    /// Non-blocking; the current expression still compiles successfully.
94    Warning,
95    /// Blocking for *this expression only*; siblings still compile.
96    Error,
97    /// Unrecoverable; the tolerant driver aborts (under default strategy).
98    Fatal,
99}
100
101impl Severity {
102    /// Returns `true` when a diagnostic of this severity drops the current
103    /// expression (or the whole program).
104    pub fn is_blocking(self) -> bool {
105        matches!(self, Severity::Error | Severity::Fatal)
106    }
107
108    /// Short, user-facing label (`"info"`, `"warning"`, `"error"`, `"fatal"`).
109    pub fn label(self) -> &'static str {
110        match self {
111            Severity::Info => "info",
112            Severity::Warning => "warning",
113            Severity::Error => "error",
114            Severity::Fatal => "fatal",
115        }
116    }
117}
118
119impl fmt::Display for Severity {
120    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
121        f.write_str(self.label())
122    }
123}
124
125/// A single diagnostic produced during tolerant compilation.
126#[derive(Debug, Clone, PartialEq, Eq)]
127pub struct Diagnostic {
128    /// Severity of the diagnostic.
129    pub severity: Severity,
130    /// Human-readable message.
131    pub message: String,
132    /// Optional source location.
133    pub location: Option<SourceSpan>,
134    /// Index into the slice passed to the tolerant compiler, if the
135    /// diagnostic is tied to one particular expression.
136    pub expression_index: Option<usize>,
137}
138
139impl Diagnostic {
140    /// Construct a diagnostic from scratch.
141    pub fn new(severity: Severity, message: impl Into<String>) -> Self {
142        Self {
143            severity,
144            message: message.into(),
145            location: None,
146            expression_index: None,
147        }
148    }
149
150    /// Convenience constructor for [`Severity::Info`] diagnostics.
151    pub fn info(message: impl Into<String>) -> Self {
152        Self::new(Severity::Info, message)
153    }
154
155    /// Convenience constructor for [`Severity::Warning`] diagnostics.
156    pub fn warning(message: impl Into<String>) -> Self {
157        Self::new(Severity::Warning, message)
158    }
159
160    /// Convenience constructor for [`Severity::Error`] diagnostics.
161    pub fn error(message: impl Into<String>) -> Self {
162        Self::new(Severity::Error, message)
163    }
164
165    /// Convenience constructor for [`Severity::Fatal`] diagnostics.
166    pub fn fatal(message: impl Into<String>) -> Self {
167        Self::new(Severity::Fatal, message)
168    }
169
170    /// Attach an expression index (builder-style).
171    pub fn with_expression_index(mut self, idx: usize) -> Self {
172        self.expression_index = Some(idx);
173        self
174    }
175
176    /// Attach a source span (builder-style).
177    pub fn with_location(mut self, span: SourceSpan) -> Self {
178        self.location = Some(span);
179        self
180    }
181
182    /// Returns `true` when this diagnostic is blocking
183    /// (i.e. [`Severity::Error`] or [`Severity::Fatal`]).
184    pub fn is_blocking(&self) -> bool {
185        self.severity.is_blocking()
186    }
187
188    /// Returns `true` when this diagnostic is fatal
189    /// (i.e. [`Severity::Fatal`]).
190    pub fn is_fatal(&self) -> bool {
191        self.severity == Severity::Fatal
192    }
193}
194
195impl fmt::Display for Diagnostic {
196    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
197        write!(f, "[{}]", self.severity)?;
198        if let Some(idx) = self.expression_index {
199            write!(f, "[expr#{}]", idx)?;
200        }
201        if let Some(span) = &self.location {
202            write!(f, " ({})", span)?;
203        }
204        write!(f, " {}", self.message)
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211
212    #[test]
213    fn severity_ordering() {
214        assert!(Severity::Info < Severity::Warning);
215        assert!(Severity::Warning < Severity::Error);
216        assert!(Severity::Error < Severity::Fatal);
217    }
218
219    #[test]
220    fn severity_is_blocking() {
221        assert!(!Severity::Info.is_blocking());
222        assert!(!Severity::Warning.is_blocking());
223        assert!(Severity::Error.is_blocking());
224        assert!(Severity::Fatal.is_blocking());
225    }
226
227    #[test]
228    fn diagnostic_builders() {
229        let d = Diagnostic::error("boom").with_expression_index(7);
230        assert_eq!(d.severity, Severity::Error);
231        assert_eq!(d.message, "boom");
232        assert_eq!(d.expression_index, Some(7));
233        assert!(d.is_blocking());
234        assert!(!d.is_fatal());
235    }
236
237    #[test]
238    fn diagnostic_display_includes_index_and_span() {
239        let d = Diagnostic::warning("possible issue")
240            .with_expression_index(3)
241            .with_location(SourceSpan::with_source(10, 20, "main.tl"));
242        let s = format!("{}", d);
243        assert!(s.contains("warning"));
244        assert!(s.contains("expr#3"));
245        assert!(s.contains("main.tl:10..20"));
246        assert!(s.contains("possible issue"));
247    }
248
249    #[test]
250    fn source_span_basics() {
251        let sp = SourceSpan::new(3, 7);
252        assert_eq!(sp.len(), 4);
253        assert!(!sp.is_empty());
254        let empty = SourceSpan::new(5, 5);
255        assert!(empty.is_empty());
256    }
257}