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}