Skip to main content

stack_compiler/
diagnostic.rs

1//! Structured diagnostics emitted by compiler stages.
2
3/// A one-based source position with its zero-based UTF-8 byte offset.
4#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
5pub struct SourcePosition {
6    /// Zero-based byte offset in the original UTF-8 source.
7    pub byte_offset: usize,
8    /// One-based source line.
9    pub line: usize,
10    /// One-based Unicode scalar column.
11    pub column: usize,
12}
13
14impl SourcePosition {
15    /// Creates the first position in a source document.
16    pub const fn start() -> Self {
17        Self {
18            byte_offset: 0,
19            line: 1,
20            column: 1,
21        }
22    }
23}
24
25/// An end-exclusive source span.
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
27pub struct Span {
28    /// Inclusive start position.
29    pub start: SourcePosition,
30    /// Exclusive end position.
31    pub end: SourcePosition,
32}
33
34impl Span {
35    /// Creates an empty span at one position.
36    pub const fn point(position: SourcePosition) -> Self {
37        Self {
38            start: position,
39            end: position,
40        }
41    }
42
43    /// Creates a span covering both input spans.
44    pub fn covering(start: Self, end: Self) -> Self {
45        Self {
46            start: start.start,
47            end: end.end,
48        }
49    }
50}
51
52/// A value paired with the source span that authored it.
53#[derive(Debug, Clone, PartialEq, Eq)]
54pub struct Spanned<T> {
55    /// Decoded or parsed value.
56    pub value: T,
57    /// Source span for the value.
58    pub span: Span,
59}
60
61impl<T> Spanned<T> {
62    /// Creates a spanned value.
63    pub const fn new(value: T, span: Span) -> Self {
64        Self { value, span }
65    }
66}
67
68/// Diagnostic severity defined by the Stack specification.
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum Severity {
71    /// The source cannot produce normalized IR.
72    Error,
73    /// The source remains valid but deserves attention.
74    Warning,
75}
76
77/// Additional source context related to a diagnostic.
78#[derive(Debug, Clone, PartialEq, Eq)]
79pub struct RelatedInformation {
80    /// Description of the related source location.
81    pub message: String,
82    /// Related source span.
83    pub span: Span,
84}
85
86/// A portable compiler diagnostic.
87#[derive(Debug, Clone, PartialEq, Eq)]
88pub struct Diagnostic {
89    /// Stable diagnostic identifier.
90    pub code: &'static str,
91    /// Error or warning severity.
92    pub severity: Severity,
93    /// Concise human-readable description.
94    pub message: String,
95    /// Primary source span.
96    pub span: Span,
97    /// Ordered source values or constructs valid at the primary span.
98    pub expected: Vec<String>,
99    /// Optional corrective guidance.
100    pub help: Option<String>,
101    /// Other declarations or references involved in the problem.
102    pub related: Vec<RelatedInformation>,
103}
104
105impl Diagnostic {
106    pub(crate) fn error(code: &'static str, message: impl Into<String>, span: Span) -> Self {
107        Self {
108            code,
109            severity: Severity::Error,
110            message: message.into(),
111            span,
112            expected: Vec::new(),
113            help: None,
114            related: Vec::new(),
115        }
116    }
117
118    pub(crate) fn warning(code: &'static str, message: impl Into<String>, span: Span) -> Self {
119        Self {
120            code,
121            severity: Severity::Warning,
122            message: message.into(),
123            span,
124            expected: Vec::new(),
125            help: None,
126            related: Vec::new(),
127        }
128    }
129
130    pub(crate) fn with_help(mut self, help: impl Into<String>) -> Self {
131        self.help = Some(help.into());
132        self
133    }
134
135    pub(crate) fn with_expected<I, S>(mut self, expected: I) -> Self
136    where
137        I: IntoIterator<Item = S>,
138        S: Into<String>,
139    {
140        self.expected = expected.into_iter().map(Into::into).collect();
141        self
142    }
143
144    pub(crate) fn with_related(mut self, message: impl Into<String>, span: Span) -> Self {
145        self.related.push(RelatedInformation {
146            message: message.into(),
147            span,
148        });
149        self
150    }
151}
152
153#[cfg(test)]
154mod tests {
155    use super::{Diagnostic, SourcePosition, Span};
156
157    #[test]
158    fn diagnostics_default_to_no_expected_values() {
159        let diagnostic = Diagnostic::error(
160            "STK2002",
161            "Unexpected value.",
162            Span::point(SourcePosition::start()),
163        );
164
165        assert!(diagnostic.expected.is_empty());
166    }
167
168    #[test]
169    fn diagnostics_preserve_expected_value_order() {
170        let diagnostic = Diagnostic::error(
171            "STK2002",
172            "Unknown direction.",
173            Span::point(SourcePosition::start()),
174        )
175        .with_expected(["right", "down"]);
176
177        assert_eq!(diagnostic.expected, ["right", "down"]);
178    }
179}