Skip to main content

rucc_lex/
keyword.rs

1//! Keywords, and which ones the dialect has.
2//!
3//! Design: `spec/06-lexer-and-parser.md` section 6.1.
4//!
5//! Phase 7 turns an identifier into a keyword when the active `-std=` says that spelling is
6//! one. Doing that with a string comparison, or with a hash lookup on the text, would put a
7//! second pass over every identifier in the file right after the scan that already interned
8//! it. So the keywords are interned first, before anything else, which makes their symbols one
9//! contiguous run at the bottom of the table. Recognition is then a subtraction, a bounds
10//! check and a byte load, and every identifier a program actually declares fails the bounds
11//! check on the first instruction.
12//!
13//! The dialect gate is part of the same load. Whether a spelling is a keyword depends on the
14//! dialect, and the dialect is fixed for the whole compilation, so [`Keywords::new`] resolves
15//! it once: each entry holds the keyword it means in this dialect, or nothing when the
16//! spelling is an ordinary identifier here. `restrict` is a keyword from C99 and a variable
17//! name in C89, `typeof` is one in C23 and in the GNU dialects and not in `-std=c17`, and
18//! `__typeof__` is one everywhere, which is why headers are written with the ugly spelling.
19//!
20//! Which spelling is a keyword in which dialect was measured rather than read out of the
21//! standard, because the standard does not describe the GNU dialects and the underscore
22//! spellings are on in dialects that predate them. Every identifier below was compiled as
23//! `void f(void) { int KW = 0; (void)KW; }` against gcc 13.3 on x86-64 Linux and against
24//! clang, in each of c89, gnu89, c99, gnu99, c11, gnu11, c17, gnu17, c23 and gnu23, with two
25//! ordinary identifiers along for the ride to catch a probe that had stopped measuring
26//! anything. The two compilers agree except where noted.
27//!
28//! Three deliberate differences from gcc 13.3:
29//!
30//! `_BitInt` is a keyword here in every dialect. gcc 13.3 does not have the type at all and
31//! gcc 14 does; clang has it and makes the spelling a keyword in every dialect. It is in the
32//! reserved namespace, so nothing legal can notice.
33//!
34//! `__float128` and `__bf16` are not keywords. gcc registers them as predefined type names,
35//! which a declaration is allowed to shadow, and `void f(void) { int __float128 = 0; }`
36//! compiles there. clang makes both of them keywords and rejects it. We follow gcc, so they
37//! belong with the other predefined types rather than here.
38//!
39//! gcc also reserves `_Sat`, `_Fract`, `_Accum`, `__seg_fs` and `__seg_gs` in the GNU
40//! dialects. They are left out until the fixed point types and the named address spaces are
41//! implemented, because a keyword the parser can only refuse is worse for a program than an
42//! identifier it can at least read.
43
44use rucc_base::{Interner, Symbol};
45use rucc_session::Std;
46
47/// A keyword, meaning a spelling the grammar knows rather than a name a program chose.
48///
49/// One variant per meaning, not per spelling. `__inline__` and `inline` are the same keyword
50/// because they are the same declaration specifier, and a parser that had to know which of
51/// them was written would be carrying the difference all the way to the AST for nothing.
52/// Where two spellings mean genuinely different things they stay apart: `__alignof__` is
53/// [`Keyword::GnuAlignof`] rather than [`Keyword::Alignof`], because GNU's asks for the
54/// alignment the target prefers and C's asks for the one the ABI requires, and on i386 they
55/// disagree about `double`.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
57pub enum Keyword {
58    /// `auto`.
59    Auto,
60    /// `break`.
61    Break,
62    /// `case`.
63    Case,
64    /// `char`.
65    Char,
66    /// `const`, and the GNU spelling `__const`.
67    Const,
68    /// `continue`.
69    Continue,
70    /// `default`.
71    Default,
72    /// `do`.
73    Do,
74    /// `double`.
75    Double,
76    /// `else`.
77    Else,
78    /// `enum`.
79    Enum,
80    /// `extern`.
81    Extern,
82    /// `float`.
83    Float,
84    /// `for`.
85    For,
86    /// `goto`.
87    Goto,
88    /// `if`.
89    If,
90    /// `int`.
91    Int,
92    /// `long`.
93    Long,
94    /// `register`.
95    Register,
96    /// `return`.
97    Return,
98    /// `short`.
99    Short,
100    /// `signed`, and the GNU spelling `__signed__`.
101    Signed,
102    /// `sizeof`.
103    Sizeof,
104    /// `static`.
105    Static,
106    /// `struct`.
107    Struct,
108    /// `switch`.
109    Switch,
110    /// `typedef`.
111    Typedef,
112    /// `union`.
113    Union,
114    /// `unsigned`.
115    Unsigned,
116    /// `void`.
117    Void,
118    /// `volatile`, and the GNU spelling `__volatile__`.
119    Volatile,
120    /// `while`.
121    While,
122    /// `inline`, from C99, and the GNU spelling `__inline__`.
123    Inline,
124    /// `restrict`, from C99, and the GNU spelling `__restrict__`.
125    Restrict,
126    /// `_Bool`, and `bool` from C23.
127    Bool,
128    /// `_Complex`, and the GNU spelling `__complex__`.
129    Complex,
130    /// `_Imaginary`.
131    Imaginary,
132    /// `_Alignas`, and `alignas` from C23.
133    Alignas,
134    /// `_Alignof`, and `alignof` from C23.
135    Alignof,
136    /// `_Atomic`.
137    Atomic,
138    /// `_Generic`.
139    Generic,
140    /// `_Noreturn`.
141    Noreturn,
142    /// `_Static_assert`, and `static_assert` from C23.
143    StaticAssert,
144    /// `_Thread_local`, `thread_local` from C23, and the GNU spelling `__thread`.
145    ThreadLocal,
146    /// `_BitInt`.
147    BitInt,
148    /// `_Decimal32`.
149    Decimal32,
150    /// `_Decimal64`.
151    Decimal64,
152    /// `_Decimal128`.
153    Decimal128,
154    /// `_Float16`.
155    Float16,
156    /// `_Float32`.
157    Float32,
158    /// `_Float64`.
159    Float64,
160    /// `_Float128`.
161    Float128,
162    /// `_Float32x`.
163    Float32x,
164    /// `_Float64x`.
165    Float64x,
166    /// `_Float128x`.
167    Float128x,
168    /// `constexpr`, from C23.
169    Constexpr,
170    /// `false`, from C23.
171    False,
172    /// `nullptr`, from C23.
173    Nullptr,
174    /// `true`, from C23.
175    True,
176    /// `typeof`, from C23 and from the GNU dialects, and the spelling `__typeof__`.
177    Typeof,
178    /// `typeof_unqual`, from C23, and the spelling `__typeof_unqual__`.
179    TypeofUnqual,
180    /// `asm`, in the GNU dialects, and the spelling `__asm__`.
181    Asm,
182    /// `__attribute__`.
183    Attribute,
184    /// `__auto_type`, which is not `auto`: it deduces from an initialiser in every dialect.
185    AutoType,
186    /// `__alignof__`, which asks for the preferred alignment rather than the required one.
187    GnuAlignof,
188    /// `__extension__`, which turns off the pedantic diagnostics for one expression.
189    Extension,
190    /// `__imag__`.
191    Imag,
192    /// `__real__`.
193    Real,
194    /// `__int128`.
195    Int128,
196    /// `__label__`, which declares a local label in a statement expression.
197    Label,
198    /// `__builtin_offsetof`, which is syntax rather than a function because it takes a type.
199    BuiltinOffsetof,
200    /// `__builtin_choose_expr`.
201    BuiltinChooseExpr,
202    /// `__builtin_types_compatible_p`.
203    BuiltinTypesCompatibleP,
204    /// `__builtin_va_arg`.
205    BuiltinVaArg,
206}
207
208impl Keyword {
209    /// The spelling to print in a diagnostic, which is the standard one where there is one.
210    ///
211    /// This walks the table, because it is only ever reached while writing a message and a
212    /// second array indexed by the enum would be one more place for the two to disagree.
213    #[must_use]
214    pub fn as_str(self) -> &'static str {
215        KEYWORDS
216            .iter()
217            .find(|entry| entry.keyword == self)
218            .map_or("keyword", |entry| entry.spelling)
219    }
220}
221
222/// The keywords of one dialect, ready to be looked up by symbol.
223///
224/// Built once per compilation, against the interner that compilation will use, before any
225/// source has been read.
226#[derive(Debug)]
227pub struct Keywords {
228    /// The symbol of the first entry. Everything below this is not a keyword, and so is
229    /// everything at or past the end of `active`.
230    base: u32,
231    /// The keyword each spelling means in this dialect, indexed by symbol minus `base`, and
232    /// [`None`] for a spelling this dialect leaves as an ordinary identifier.
233    active: Box<[Option<Keyword>]>,
234}
235
236impl Keywords {
237    /// Interns every keyword spelling and resolves which of them this dialect has.
238    ///
239    /// # Panics
240    ///
241    /// Panics if `interner` has already been given one of these spellings, since the symbols
242    /// would no longer be one run and every lookup after that would be wrong. Build this
243    /// first, immediately after the interner itself.
244    #[must_use]
245    pub fn new(interner: &mut Interner, std: Std, gnu: bool) -> Keywords {
246        let dialect = mask(std, gnu);
247        let mut base = 0;
248        let mut active = Vec::with_capacity(KEYWORDS.len());
249        for entry in KEYWORDS {
250            let symbol = interner.intern(entry.spelling).raw();
251            if active.is_empty() {
252                base = symbol;
253            }
254            let want = base + u32::try_from(active.len()).expect("the table is not that long");
255            assert!(
256                symbol == want,
257                "`{}` was interned before the keyword table was built",
258                entry.spelling
259            );
260            active.push((entry.dialects & dialect != 0).then_some(entry.keyword));
261        }
262        Keywords { base, active: active.into_boxed_slice() }
263    }
264
265    /// The keyword `symbol` is in this dialect, and [`None`] when it is an identifier.
266    #[must_use]
267    #[inline]
268    pub fn get(&self, symbol: Symbol) -> Option<Keyword> {
269        let index = symbol.raw().checked_sub(self.base)?;
270        // A `usize` cast rather than a conversion: the index is already known to fit, because
271        // the slice it indexes was built from symbols this interner handed out.
272        *self.active.get(index as usize)?
273    }
274
275    /// Whether `symbol` is a keyword in this dialect.
276    #[must_use]
277    #[inline]
278    pub fn contains(&self, symbol: Symbol) -> bool {
279        self.get(symbol).is_some()
280    }
281
282    /// How many spellings the table holds, active in this dialect or not.
283    #[must_use]
284    pub fn len(&self) -> usize {
285        self.active.len()
286    }
287
288    /// Whether the table is empty, which it never is.
289    #[must_use]
290    pub fn is_empty(&self) -> bool {
291        self.active.is_empty()
292    }
293}
294
295/// One bit per dialect, plus one for the GNU extensions.
296const C89: u8 = 1 << 0;
297const C99: u8 = 1 << 1;
298const C11: u8 = 1 << 2;
299const C17: u8 = 1 << 3;
300const C23: u8 = 1 << 4;
301const GNU: u8 = 1 << 5;
302
303/// A spelling that is a keyword in every dialect, GNU or not.
304const ALWAYS: u8 = C89 | C99 | C11 | C17 | C23 | GNU;
305/// From C99 onwards, and not in `-std=gnu89`. This is `restrict`, and it is the one place the
306/// GNU dialects are not a superset: gcc and clang both keep `restrict` out of `gnu89` and
307/// offer `__restrict` there instead.
308const SINCE_C99: u8 = C99 | C11 | C17 | C23;
309/// From C99 onwards, and in every GNU dialect including `gnu89`. This is `inline`.
310const SINCE_C99_OR_GNU: u8 = SINCE_C99 | GNU;
311/// C23 only. The lowercase spellings of the C11 keywords are here, and so is the rest of what
312/// C23 added, and `-std=gnu17` does not have any of them.
313const SINCE_C23: u8 = C23;
314/// C23, and every GNU dialect. This is `typeof`, which gcc has had for decades and which C23
315/// standardised, so `-std=c17` is the only place it is a variable name.
316const SINCE_C23_OR_GNU: u8 = C23 | GNU;
317/// The GNU dialects only. This is `asm`, which is a keyword in `gnu23` and an identifier in
318/// `c23`, where `__asm__` has to be written instead.
319const GNU_ONLY: u8 = GNU;
320
321/// A spelling, what it means, and where it is a keyword.
322struct Entry {
323    /// The spelling as it appears in source.
324    spelling: &'static str,
325    /// What the grammar makes of it.
326    keyword: Keyword,
327    /// The dialects it is a keyword in, as a mask of the bits above.
328    dialects: u8,
329}
330
331/// Shorthand, so that the table below reads as a table rather than a page of struct literals.
332const fn e(spelling: &'static str, keyword: Keyword, dialects: u8) -> Entry {
333    Entry { spelling, keyword, dialects }
334}
335
336/// Every keyword spelling in every dialect we support.
337///
338/// The order is the interning order and so decides the symbols, which nothing may depend on;
339/// it is grouped by where each spelling came from because that is how it is checked against a
340/// compiler. The first entry for a keyword is the spelling [`Keyword::as_str`] prints.
341static KEYWORDS: &[Entry] = &[
342    // The C89 keywords. Nothing has ever removed one, so all of them are unconditional.
343    e("auto", Keyword::Auto, ALWAYS),
344    e("break", Keyword::Break, ALWAYS),
345    e("case", Keyword::Case, ALWAYS),
346    e("char", Keyword::Char, ALWAYS),
347    e("const", Keyword::Const, ALWAYS),
348    e("continue", Keyword::Continue, ALWAYS),
349    e("default", Keyword::Default, ALWAYS),
350    e("do", Keyword::Do, ALWAYS),
351    e("double", Keyword::Double, ALWAYS),
352    e("else", Keyword::Else, ALWAYS),
353    e("enum", Keyword::Enum, ALWAYS),
354    e("extern", Keyword::Extern, ALWAYS),
355    e("float", Keyword::Float, ALWAYS),
356    e("for", Keyword::For, ALWAYS),
357    e("goto", Keyword::Goto, ALWAYS),
358    e("if", Keyword::If, ALWAYS),
359    e("int", Keyword::Int, ALWAYS),
360    e("long", Keyword::Long, ALWAYS),
361    e("register", Keyword::Register, ALWAYS),
362    e("return", Keyword::Return, ALWAYS),
363    e("short", Keyword::Short, ALWAYS),
364    e("signed", Keyword::Signed, ALWAYS),
365    e("sizeof", Keyword::Sizeof, ALWAYS),
366    e("static", Keyword::Static, ALWAYS),
367    e("struct", Keyword::Struct, ALWAYS),
368    e("switch", Keyword::Switch, ALWAYS),
369    e("typedef", Keyword::Typedef, ALWAYS),
370    e("union", Keyword::Union, ALWAYS),
371    e("unsigned", Keyword::Unsigned, ALWAYS),
372    e("void", Keyword::Void, ALWAYS),
373    e("volatile", Keyword::Volatile, ALWAYS),
374    e("while", Keyword::While, ALWAYS),
375    // The two C99 additions that are ordinary words. Everything else C99 and C11 added is
376    // spelled with a leading underscore precisely so that it could be turned on in the
377    // older dialects without breaking a program that had used the name, and both
378    // compilers do exactly that.
379    e("inline", Keyword::Inline, SINCE_C99_OR_GNU),
380    e("restrict", Keyword::Restrict, SINCE_C99),
381    e("_Bool", Keyword::Bool, ALWAYS),
382    e("_Complex", Keyword::Complex, ALWAYS),
383    e("_Imaginary", Keyword::Imaginary, ALWAYS),
384    e("_Alignas", Keyword::Alignas, ALWAYS),
385    e("_Alignof", Keyword::Alignof, ALWAYS),
386    e("_Atomic", Keyword::Atomic, ALWAYS),
387    e("_Generic", Keyword::Generic, ALWAYS),
388    e("_Noreturn", Keyword::Noreturn, ALWAYS),
389    e("_Static_assert", Keyword::StaticAssert, ALWAYS),
390    e("_Thread_local", Keyword::ThreadLocal, ALWAYS),
391    e("_BitInt", Keyword::BitInt, ALWAYS),
392    e("_Decimal32", Keyword::Decimal32, ALWAYS),
393    e("_Decimal64", Keyword::Decimal64, ALWAYS),
394    e("_Decimal128", Keyword::Decimal128, ALWAYS),
395    e("_Float16", Keyword::Float16, ALWAYS),
396    e("_Float32", Keyword::Float32, ALWAYS),
397    e("_Float64", Keyword::Float64, ALWAYS),
398    e("_Float128", Keyword::Float128, ALWAYS),
399    e("_Float32x", Keyword::Float32x, ALWAYS),
400    e("_Float64x", Keyword::Float64x, ALWAYS),
401    e("_Float128x", Keyword::Float128x, ALWAYS),
402    // C23, which spelled the C11 keywords as words and added its own. A program that used
403    // `bool` as a variable name still compiles in every earlier dialect, which is the
404    // whole reason this table is gated rather than fixed.
405    e("alignas", Keyword::Alignas, SINCE_C23),
406    e("alignof", Keyword::Alignof, SINCE_C23),
407    e("bool", Keyword::Bool, SINCE_C23),
408    e("constexpr", Keyword::Constexpr, SINCE_C23),
409    e("false", Keyword::False, SINCE_C23),
410    e("nullptr", Keyword::Nullptr, SINCE_C23),
411    e("static_assert", Keyword::StaticAssert, SINCE_C23),
412    e("thread_local", Keyword::ThreadLocal, SINCE_C23),
413    e("true", Keyword::True, SINCE_C23),
414    e("typeof", Keyword::Typeof, SINCE_C23_OR_GNU),
415    e("typeof_unqual", Keyword::TypeofUnqual, SINCE_C23),
416    e("asm", Keyword::Asm, GNU_ONLY),
417    // The GNU spellings. All of them are in the reserved namespace, so gcc turns them on
418    // in every dialect including `-std=c89`, and a header that has to work under `-std=`
419    // anything is written with these rather than with the words above.
420    e("__asm", Keyword::Asm, ALWAYS),
421    e("__asm__", Keyword::Asm, ALWAYS),
422    e("__alignof", Keyword::GnuAlignof, ALWAYS),
423    e("__alignof__", Keyword::GnuAlignof, ALWAYS),
424    e("__attribute", Keyword::Attribute, ALWAYS),
425    e("__attribute__", Keyword::Attribute, ALWAYS),
426    e("__auto_type", Keyword::AutoType, ALWAYS),
427    e("__complex", Keyword::Complex, ALWAYS),
428    e("__complex__", Keyword::Complex, ALWAYS),
429    e("__const", Keyword::Const, ALWAYS),
430    e("__extension__", Keyword::Extension, ALWAYS),
431    e("__imag", Keyword::Imag, ALWAYS),
432    e("__imag__", Keyword::Imag, ALWAYS),
433    e("__inline", Keyword::Inline, ALWAYS),
434    e("__inline__", Keyword::Inline, ALWAYS),
435    e("__int128", Keyword::Int128, ALWAYS),
436    e("__label__", Keyword::Label, ALWAYS),
437    e("__real", Keyword::Real, ALWAYS),
438    e("__real__", Keyword::Real, ALWAYS),
439    e("__restrict", Keyword::Restrict, ALWAYS),
440    e("__restrict__", Keyword::Restrict, ALWAYS),
441    e("__signed", Keyword::Signed, ALWAYS),
442    e("__signed__", Keyword::Signed, ALWAYS),
443    // gcc's own diagnostics keep `__thread` and `_Thread_local` apart, but in C they are
444    // one storage class with two spellings, so the parser is given one keyword.
445    e("__thread", Keyword::ThreadLocal, ALWAYS),
446    e("__typeof", Keyword::Typeof, ALWAYS),
447    e("__typeof__", Keyword::Typeof, ALWAYS),
448    e("__typeof_unqual", Keyword::TypeofUnqual, ALWAYS),
449    e("__typeof_unqual__", Keyword::TypeofUnqual, ALWAYS),
450    e("__volatile", Keyword::Volatile, ALWAYS),
451    e("__volatile__", Keyword::Volatile, ALWAYS),
452    // The builtins that are syntax rather than functions, because an argument of theirs is
453    // a type name. Everything else called `__builtin_` is an ordinary identifier that
454    // resolves to a declaration, and belongs nowhere near this table.
455    e("__builtin_offsetof", Keyword::BuiltinOffsetof, ALWAYS),
456    e("__builtin_choose_expr", Keyword::BuiltinChooseExpr, ALWAYS),
457    e("__builtin_types_compatible_p", Keyword::BuiltinTypesCompatibleP, ALWAYS),
458    e("__builtin_va_arg", Keyword::BuiltinVaArg, ALWAYS),
459];
460
461/// The bits a dialect matches, which is its own and the GNU one when the extensions are on.
462const fn mask(std: Std, gnu: bool) -> u8 {
463    let dialect = match std {
464        Std::C89 => C89,
465        Std::C99 => C99,
466        Std::C11 => C11,
467        Std::C17 => C17,
468        Std::C23 => C23,
469    };
470    if gnu { dialect | GNU } else { dialect }
471}
472
473#[cfg(test)]
474mod tests {
475    use super::*;
476
477    /// The keywords of one dialect, and an interner that has them and nothing else.
478    fn build(std: Std, gnu: bool) -> (Keywords, Interner) {
479        let mut interner = Interner::new();
480        let keywords = Keywords::new(&mut interner, std, gnu);
481        (keywords, interner)
482    }
483
484    /// What `text` means in this dialect, having been interned the way the scanner would.
485    fn lookup(std: Std, gnu: bool, text: &str) -> Option<Keyword> {
486        let (keywords, mut interner) = build(std, gnu);
487        keywords.get(interner.intern(text))
488    }
489
490    #[test]
491    fn a_word_the_language_has_always_had_is_a_keyword_in_every_dialect() {
492        for std in [Std::C89, Std::C99, Std::C11, Std::C17, Std::C23] {
493            for gnu in [false, true] {
494                assert_eq!(lookup(std, gnu, "int"), Some(Keyword::Int));
495                assert_eq!(lookup(std, gnu, "sizeof"), Some(Keyword::Sizeof));
496                assert_eq!(lookup(std, gnu, "_Complex"), Some(Keyword::Complex));
497            }
498        }
499    }
500
501    #[test]
502    fn a_name_a_program_chose_is_never_a_keyword() {
503        // Including one that only just misses, and one that reads like a keyword and is not.
504        for name in ["x", "intx", "in", "INT", "fortran", "ordinary", "__builtin_expect"] {
505            assert_eq!(lookup(Std::C23, true, name), None, "{name} is not a keyword");
506        }
507    }
508
509    #[test]
510    fn restrict_arrived_in_c99_and_gnu89_did_not_get_it_early() {
511        // Measured: gcc and clang both leave `restrict` out of `-std=gnu89`, which is the one
512        // place the GNU dialect is not a superset of the standard one it is based on.
513        assert_eq!(lookup(Std::C89, false, "restrict"), None);
514        assert_eq!(lookup(Std::C89, true, "restrict"), None);
515        assert_eq!(lookup(Std::C99, false, "restrict"), Some(Keyword::Restrict));
516        // `__restrict__` is how a header written for both says it, and it works in c89.
517        assert_eq!(lookup(Std::C89, false, "__restrict__"), Some(Keyword::Restrict));
518    }
519
520    #[test]
521    fn inline_arrived_in_c99_and_gnu89_did_get_it_early() {
522        assert_eq!(lookup(Std::C89, false, "inline"), None);
523        assert_eq!(lookup(Std::C89, true, "inline"), Some(Keyword::Inline));
524        assert_eq!(lookup(Std::C99, false, "inline"), Some(Keyword::Inline));
525    }
526
527    #[test]
528    fn typeof_is_a_gnu_extension_that_c23_made_standard() {
529        assert_eq!(lookup(Std::C17, false, "typeof"), None);
530        assert_eq!(lookup(Std::C17, true, "typeof"), Some(Keyword::Typeof));
531        assert_eq!(lookup(Std::C23, false, "typeof"), Some(Keyword::Typeof));
532        // `typeof_unqual` is the C23 half only, which is what both compilers do.
533        assert_eq!(lookup(Std::C17, true, "typeof_unqual"), None);
534        assert_eq!(lookup(Std::C23, false, "typeof_unqual"), Some(Keyword::TypeofUnqual));
535        assert_eq!(lookup(Std::C17, false, "__typeof__"), Some(Keyword::Typeof));
536    }
537
538    #[test]
539    fn asm_is_the_one_word_c23_still_does_not_have() {
540        assert_eq!(lookup(Std::C23, false, "asm"), None);
541        assert_eq!(lookup(Std::C23, true, "asm"), Some(Keyword::Asm));
542        assert_eq!(lookup(Std::C89, false, "__asm__"), Some(Keyword::Asm));
543    }
544
545    #[test]
546    fn the_c23_words_are_variable_names_in_every_earlier_dialect() {
547        let added = [
548            ("alignas", Keyword::Alignas),
549            ("alignof", Keyword::Alignof),
550            ("bool", Keyword::Bool),
551            ("constexpr", Keyword::Constexpr),
552            ("false", Keyword::False),
553            ("nullptr", Keyword::Nullptr),
554            ("static_assert", Keyword::StaticAssert),
555            ("thread_local", Keyword::ThreadLocal),
556            ("true", Keyword::True),
557        ];
558        for (spelling, keyword) in added {
559            assert_eq!(lookup(Std::C17, true, spelling), None, "{spelling} in gnu17");
560            assert_eq!(lookup(Std::C23, false, spelling), Some(keyword), "{spelling} in c23");
561        }
562        // The underscore spellings they replaced go on working, which is what lets one header
563        // serve both.
564        assert_eq!(lookup(Std::C17, false, "_Static_assert"), Some(Keyword::StaticAssert));
565        assert_eq!(lookup(Std::C23, false, "_Static_assert"), Some(Keyword::StaticAssert));
566    }
567
568    #[test]
569    fn two_spellings_of_one_thing_are_one_keyword() {
570        for spelling in ["const", "__const"] {
571            assert_eq!(lookup(Std::C23, true, spelling), Some(Keyword::Const));
572        }
573        for spelling in ["_Thread_local", "thread_local", "__thread"] {
574            assert_eq!(lookup(Std::C23, true, spelling), Some(Keyword::ThreadLocal));
575        }
576        // And the one pair that looks like two spellings and is not. GNU's `__alignof__`
577        // reports the preferred alignment, C's `_Alignof` the required one.
578        assert_ne!(
579            lookup(Std::C23, true, "__alignof__"),
580            lookup(Std::C23, true, "_Alignof"),
581            "the two alignments are different questions"
582        );
583    }
584
585    #[test]
586    fn every_keyword_prints_a_spelling_that_is_that_keyword() {
587        for entry in KEYWORDS {
588            let printed = entry.keyword.as_str();
589            let found = KEYWORDS
590                .iter()
591                .find(|other| other.spelling == printed)
592                .unwrap_or_else(|| panic!("{printed} is not in the table"));
593            assert_eq!(found.keyword, entry.keyword, "{printed} prints for the wrong keyword");
594        }
595    }
596
597    #[test]
598    fn no_spelling_is_in_the_table_twice() {
599        // A repeat would be interned once, the run of symbols would be short by one, and
600        // `Keywords::new` would refuse to build at all. Better to say why here.
601        let mut seen: Vec<&str> = KEYWORDS.iter().map(|entry| entry.spelling).collect();
602        seen.sort_unstable();
603        let count = seen.len();
604        seen.dedup();
605        assert_eq!(seen.len(), count, "a spelling appears twice in the table");
606    }
607
608    #[test]
609    fn recognition_does_not_depend_on_what_was_interned_afterwards() {
610        // The property the whole design rests on: the keywords are one run at the bottom of
611        // the table, so an identifier interned later cannot land inside it however many there
612        // are.
613        let (keywords, mut interner) = build(Std::C23, true);
614        for i in 0..1000 {
615            let symbol = interner.intern(&format!("name{i}"));
616            assert_eq!(keywords.get(symbol), None);
617        }
618        assert_eq!(keywords.get(interner.intern("while")), Some(Keyword::While));
619    }
620
621    #[test]
622    #[should_panic(expected = "`static` was interned before the keyword table was built")]
623    fn an_interner_that_already_has_a_keyword_in_it_is_refused() {
624        // Silently building a table whose symbols are not one run would mean a compiler that
625        // recognised the wrong words, which is not a failure anybody would find quickly.
626        let mut interner = Interner::new();
627        interner.intern("static");
628        let _ = Keywords::new(&mut interner, Std::C23, true);
629    }
630
631    #[test]
632    fn a_lookup_is_a_bounds_check_on_one_run_of_symbols() {
633        let (keywords, _) = build(Std::C23, true);
634        assert!(!keywords.is_empty());
635        assert_eq!(keywords.len(), KEYWORDS.len());
636    }
637}