Skip to main content

diag_lang/
code.rs

1//! Diagnostic codes: the stable, machine-readable name of a kind of diagnostic.
2
3use core::fmt;
4
5/// The longest prefix a [`Code`] accepts, in letters.
6const MAX_PREFIX: usize = 4;
7
8/// The largest number a [`Code`] accepts: codes are written with exactly four
9/// digits.
10const MAX_NUMBER: u16 = 9999;
11
12/// A diagnostic code such as `E0001`, `T0042`, or `KRA0013`: an uppercase
13/// prefix naming the registry that owns it, and a four-digit number within that
14/// registry.
15///
16/// A code is the stable identity of a *kind* of diagnostic. Messages can be
17/// reworded between releases; the code does not change, so tools key on it: an
18/// editor links it to documentation, a lint configuration silences it, a test
19/// asserts on it, and a filter separates lexical errors from parse errors by
20/// range without parsing message text. The renderer shows it in the header,
21/// rustc-style (`error[E0001]: …`), and the JSON output carries it as a string.
22///
23/// # Format
24///
25/// One to four ASCII uppercase letters (`A`–`Z`) followed by a number from `0`
26/// to `9999`, always written with exactly four digits: `Code::new("E", 1)`
27/// displays as `E0001`. The fixed width means a code's text and its value
28/// round-trip exactly ([`Code::parse`] accepts precisely what `Display`
29/// writes), and codes of one prefix sort the same as text and as numbers.
30///
31/// # Registries and ranges
32///
33/// The prefix names the registry; the number range within it names the phase.
34/// Codes in use across the `-lang` family:
35///
36/// | Prefix | Owner | Range |
37/// |---|---|---|
38/// | `E` | Runtime errors: OPS | `E0001`–`E0099` |
39/// | `E` | Runtime errors: LSB | `E0100`–`E0199` |
40/// | `T` | Type checking (typeck-lang) | `T0001`– |
41///
42/// A language front end takes its own prefix (Kraken uses `KRA`) and is
43/// recommended to split its numbers by phase, so that "is this a lexical
44/// error?" is a range check:
45///
46/// | Range | Phase |
47/// |---|---|
48/// | `0001`–`0999` | Lexical: bad characters, unterminated literals, malformed numbers |
49/// | `1000`–`1999` | Parse: unexpected or missing tokens, unbalanced delimiters |
50/// | `2000`–`7999` | Semantic: name resolution, types, borrow and flow checks |
51/// | `8000`–`8999` | Lints and style warnings |
52/// | `9000`–`9999` | Internal and tool errors (`9999`: internal compiler error) |
53///
54/// This is a convention, not an enforced rule: the crate accepts any
55/// well-formed code. See `docs/API.md` for the full guidance.
56///
57/// # Examples
58///
59/// ```
60/// use diag_lang::Code;
61///
62/// // A registry declares its codes as constants.
63/// const UNTERMINATED_STRING: Code = Code::new("KRA", 1).unwrap();
64/// const UNEXPECTED_TOKEN: Code = Code::new("KRA", 1000).unwrap();
65///
66/// assert_eq!(UNTERMINATED_STRING.to_string(), "KRA0001");
67/// assert_eq!(UNEXPECTED_TOKEN.prefix(), "KRA");
68/// assert_eq!(UNEXPECTED_TOKEN.number(), 1000);
69///
70/// // The phase is a range check on the number.
71/// let is_lexical = |c: Code| c.prefix() == "KRA" && c.number() < 1000;
72/// assert!(is_lexical(UNTERMINATED_STRING));
73/// assert!(!is_lexical(UNEXPECTED_TOKEN));
74///
75/// // Text and value round-trip exactly.
76/// assert_eq!(Code::parse("KRA0001"), Some(UNTERMINATED_STRING));
77/// ```
78#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
79pub struct Code {
80    /// The prefix letters, left-aligned and padded with zero bytes. Zero sorts
81    /// below every letter, so the derived order puts `E` before `EA` before `F`.
82    prefix: [u8; MAX_PREFIX],
83    number: u16,
84}
85
86impl Code {
87    /// Builds the code `prefix` + `number`, or `None` if either is out of range.
88    ///
89    /// `prefix` must be one to four ASCII uppercase letters and `number` at
90    /// most `9999`. The function is `const`, so a registry can declare its codes
91    /// as constants with `Code::new("E", 1).unwrap()`, where a malformed code is
92    /// a compile error rather than a runtime failure.
93    ///
94    /// # Examples
95    ///
96    /// ```
97    /// use diag_lang::Code;
98    ///
99    /// const ARITH_OVERFLOW: Code = Code::new("E", 1).unwrap();
100    /// assert_eq!(ARITH_OVERFLOW.to_string(), "E0001");
101    ///
102    /// assert_eq!(Code::new("e", 1), None); // lowercase
103    /// assert_eq!(Code::new("", 1), None); // no prefix
104    /// assert_eq!(Code::new("ABCDE", 1), None); // prefix too long
105    /// assert_eq!(Code::new("E", 10_000), None); // more than four digits
106    /// ```
107    #[must_use]
108    pub const fn new(prefix: &str, number: u16) -> Option<Self> {
109        let bytes = prefix.as_bytes();
110        if bytes.is_empty() || bytes.len() > MAX_PREFIX || number > MAX_NUMBER {
111            return None;
112        }
113        let mut stored = [0u8; MAX_PREFIX];
114        let mut i = 0;
115        while i < bytes.len() {
116            if !bytes[i].is_ascii_uppercase() {
117                return None;
118            }
119            stored[i] = bytes[i];
120            i += 1;
121        }
122        Some(Self {
123            prefix: stored,
124            number,
125        })
126    }
127
128    /// Parses a code written the way [`Display`](fmt::Display) writes it: one
129    /// to four ASCII uppercase letters followed by exactly four digits.
130    ///
131    /// Returns `None` for anything else, including surrounding whitespace, a
132    /// lowercase prefix, or a number with more or fewer than four digits, so
133    /// `parse(&code.to_string()) == Some(code)` for every code and nothing else
134    /// parses.
135    ///
136    /// # Examples
137    ///
138    /// ```
139    /// use diag_lang::Code;
140    ///
141    /// let code = Code::parse("T0042").unwrap();
142    /// assert_eq!((code.prefix(), code.number()), ("T", 42));
143    ///
144    /// assert_eq!(Code::parse("T42"), None); // four digits, always
145    /// assert_eq!(Code::parse("T00042"), None);
146    /// assert_eq!(Code::parse("0042"), None); // a prefix is required
147    /// ```
148    #[must_use]
149    pub const fn parse(text: &str) -> Option<Self> {
150        let bytes = text.as_bytes();
151        // Four digits plus a prefix of one to four letters.
152        if bytes.len() < 5 || bytes.len() > MAX_PREFIX + 4 {
153            return None;
154        }
155        let split = bytes.len() - 4;
156        let mut number: u16 = 0;
157        let mut i = split;
158        while i < bytes.len() {
159            let digit = bytes[i];
160            if !digit.is_ascii_digit() {
161                return None;
162            }
163            // At most four digits, so this stays within 9999.
164            number = number * 10 + (digit - b'0') as u16;
165            i += 1;
166        }
167        let (prefix, _) = bytes.split_at(split);
168        match core::str::from_utf8(prefix) {
169            Ok(prefix) => Self::new(prefix, number),
170            Err(_) => None,
171        }
172    }
173
174    /// Returns the prefix: the registry the code belongs to, such as `"E"` or
175    /// `"KRA"`.
176    ///
177    /// # Examples
178    ///
179    /// ```
180    /// use diag_lang::Code;
181    ///
182    /// assert_eq!(Code::new("KRA", 13).unwrap().prefix(), "KRA");
183    /// ```
184    #[must_use]
185    pub fn prefix(&self) -> &str {
186        let len = self
187            .prefix
188            .iter()
189            .position(|&b| b == 0)
190            .unwrap_or(MAX_PREFIX);
191        // The stored bytes are ASCII letters by construction, so this is always
192        // `Ok`; the fallback keeps the accessor total without an `unwrap`.
193        core::str::from_utf8(self.prefix.get(..len).unwrap_or(&[])).unwrap_or("")
194    }
195
196    /// Returns the number within the prefix's registry, `0`–`9999`.
197    ///
198    /// # Examples
199    ///
200    /// ```
201    /// use diag_lang::Code;
202    ///
203    /// assert_eq!(Code::new("E", 102).unwrap().number(), 102);
204    /// ```
205    #[inline]
206    #[must_use]
207    pub const fn number(&self) -> u16 {
208        self.number
209    }
210}
211
212impl fmt::Display for Code {
213    /// Writes the prefix followed by the number as exactly four digits:
214    /// `E0001`, `KRA9999`.
215    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
216        write!(f, "{}{:04}", self.prefix(), self.number)
217    }
218}
219
220impl fmt::Debug for Code {
221    /// Writes `Code(E0001)`: the code's text is more useful than its storage.
222    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
223        write!(f, "Code({self})")
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use alloc::string::ToString;
230
231    use super::*;
232
233    #[test]
234    fn test_new_accepts_the_documented_shapes() {
235        for (prefix, number, text) in [
236            ("E", 1, "E0001"),
237            ("T", 42, "T0042"),
238            ("KRA", 9999, "KRA9999"),
239            ("ABCD", 0, "ABCD0000"),
240        ] {
241            let code = Code::new(prefix, number).unwrap();
242            assert_eq!(code.prefix(), prefix);
243            assert_eq!(code.number(), number);
244            assert_eq!(code.to_string(), text);
245            assert_eq!(Code::parse(text), Some(code));
246        }
247    }
248
249    #[test]
250    fn test_new_rejects_malformed_codes() {
251        assert_eq!(Code::new("", 1), None);
252        assert_eq!(Code::new("ABCDE", 1), None);
253        assert_eq!(Code::new("e", 1), None);
254        assert_eq!(Code::new("E1", 1), None);
255        assert_eq!(Code::new("É", 1), None);
256        assert_eq!(Code::new("E", 10_000), None);
257        assert_eq!(Code::new("E", u16::MAX), None);
258    }
259
260    #[test]
261    fn test_parse_rejects_everything_display_cannot_write() {
262        for bad in [
263            "",
264            "E",
265            "E1",
266            "E001",
267            "E00001",
268            "0001",
269            "e0001",
270            " E0001",
271            "E0001 ",
272            "E-001",
273            "E+001",
274            "ABCDE0001",
275            "E000a",
276            "É0001",
277            "E0001",
278        ] {
279            assert_eq!(Code::parse(bad), None, "{bad:?}");
280        }
281    }
282
283    #[test]
284    fn test_order_is_prefix_then_number() {
285        let e1 = Code::new("E", 1).unwrap();
286        let e2 = Code::new("E", 2).unwrap();
287        let ea = Code::new("EA", 0).unwrap();
288        let t1 = Code::new("T", 1).unwrap();
289        assert!(e1 < e2 && e2 < ea && ea < t1);
290    }
291
292    #[test]
293    fn test_debug_shows_the_text() {
294        assert_eq!(
295            alloc::format!("{:?}", Code::new("E", 7).unwrap()),
296            "Code(E0007)"
297        );
298    }
299}