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