use rucc_base::{Interner, Symbol};
use rucc_session::Std;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum Keyword {
Auto,
Break,
Case,
Char,
Const,
Continue,
Default,
Do,
Double,
Else,
Enum,
Extern,
Float,
For,
Goto,
If,
Int,
Long,
Register,
Return,
Short,
Signed,
Sizeof,
Static,
Struct,
Switch,
Typedef,
Union,
Unsigned,
Void,
Volatile,
While,
Inline,
Restrict,
Bool,
Complex,
Imaginary,
Alignas,
Alignof,
Atomic,
Generic,
Noreturn,
StaticAssert,
ThreadLocal,
BitInt,
Decimal32,
Decimal64,
Decimal128,
Float16,
Float32,
Float64,
Float128,
Float32x,
Float64x,
Float128x,
Constexpr,
False,
Nullptr,
True,
Typeof,
TypeofUnqual,
Asm,
Attribute,
AutoType,
GnuAlignof,
Extension,
Imag,
Real,
Int128,
Int128T,
UInt128T,
Label,
BuiltinOffsetof,
BuiltinChooseExpr,
BuiltinTypesCompatibleP,
BuiltinVaArg,
BuiltinVaList,
BuiltinVaStart,
BuiltinVaEnd,
BuiltinVaCopy,
}
impl Keyword {
#[must_use]
pub fn as_str(self) -> &'static str {
KEYWORDS
.iter()
.find(|entry| entry.keyword == self)
.map_or("keyword", |entry| entry.spelling)
}
}
#[derive(Debug)]
pub struct Keywords {
base: u32,
active: Box<[Option<Keyword>]>,
}
impl Keywords {
#[must_use]
pub fn new(interner: &mut Interner, std: Std, gnu: bool) -> Keywords {
let dialect = mask(std, gnu);
let mut base = 0;
let mut active = Vec::with_capacity(KEYWORDS.len());
for entry in KEYWORDS {
let symbol = interner.intern(entry.spelling).raw();
if active.is_empty() {
base = symbol;
}
let want = base + u32::try_from(active.len()).expect("the table is not that long");
assert!(
symbol == want,
"`{}` was interned before the keyword table was built",
entry.spelling
);
active.push((entry.dialects & dialect != 0).then_some(entry.keyword));
}
Keywords { base, active: active.into_boxed_slice() }
}
#[must_use]
#[inline]
pub fn get(&self, symbol: Symbol) -> Option<Keyword> {
let index = symbol.raw().checked_sub(self.base)?;
*self.active.get(index as usize)?
}
#[must_use]
#[inline]
pub fn contains(&self, symbol: Symbol) -> bool {
self.get(symbol).is_some()
}
#[must_use]
pub fn len(&self) -> usize {
self.active.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.active.is_empty()
}
}
const C89: u8 = 1 << 0;
const C99: u8 = 1 << 1;
const C11: u8 = 1 << 2;
const C17: u8 = 1 << 3;
const C23: u8 = 1 << 4;
const GNU: u8 = 1 << 5;
const ALWAYS: u8 = C89 | C99 | C11 | C17 | C23 | GNU;
const SINCE_C99: u8 = C99 | C11 | C17 | C23;
const SINCE_C99_OR_GNU: u8 = SINCE_C99 | GNU;
const SINCE_C23: u8 = C23;
const SINCE_C23_OR_GNU: u8 = C23 | GNU;
const GNU_ONLY: u8 = GNU;
struct Entry {
spelling: &'static str,
keyword: Keyword,
dialects: u8,
}
const fn e(spelling: &'static str, keyword: Keyword, dialects: u8) -> Entry {
Entry { spelling, keyword, dialects }
}
static KEYWORDS: &[Entry] = &[
e("auto", Keyword::Auto, ALWAYS),
e("break", Keyword::Break, ALWAYS),
e("case", Keyword::Case, ALWAYS),
e("char", Keyword::Char, ALWAYS),
e("const", Keyword::Const, ALWAYS),
e("continue", Keyword::Continue, ALWAYS),
e("default", Keyword::Default, ALWAYS),
e("do", Keyword::Do, ALWAYS),
e("double", Keyword::Double, ALWAYS),
e("else", Keyword::Else, ALWAYS),
e("enum", Keyword::Enum, ALWAYS),
e("extern", Keyword::Extern, ALWAYS),
e("float", Keyword::Float, ALWAYS),
e("for", Keyword::For, ALWAYS),
e("goto", Keyword::Goto, ALWAYS),
e("if", Keyword::If, ALWAYS),
e("int", Keyword::Int, ALWAYS),
e("long", Keyword::Long, ALWAYS),
e("register", Keyword::Register, ALWAYS),
e("return", Keyword::Return, ALWAYS),
e("short", Keyword::Short, ALWAYS),
e("signed", Keyword::Signed, ALWAYS),
e("sizeof", Keyword::Sizeof, ALWAYS),
e("static", Keyword::Static, ALWAYS),
e("struct", Keyword::Struct, ALWAYS),
e("switch", Keyword::Switch, ALWAYS),
e("typedef", Keyword::Typedef, ALWAYS),
e("union", Keyword::Union, ALWAYS),
e("unsigned", Keyword::Unsigned, ALWAYS),
e("void", Keyword::Void, ALWAYS),
e("volatile", Keyword::Volatile, ALWAYS),
e("while", Keyword::While, ALWAYS),
e("inline", Keyword::Inline, SINCE_C99_OR_GNU),
e("restrict", Keyword::Restrict, SINCE_C99),
e("_Bool", Keyword::Bool, ALWAYS),
e("_Complex", Keyword::Complex, ALWAYS),
e("_Imaginary", Keyword::Imaginary, ALWAYS),
e("_Alignas", Keyword::Alignas, ALWAYS),
e("_Alignof", Keyword::Alignof, ALWAYS),
e("_Atomic", Keyword::Atomic, ALWAYS),
e("_Generic", Keyword::Generic, ALWAYS),
e("_Noreturn", Keyword::Noreturn, ALWAYS),
e("_Static_assert", Keyword::StaticAssert, ALWAYS),
e("_Thread_local", Keyword::ThreadLocal, ALWAYS),
e("_BitInt", Keyword::BitInt, ALWAYS),
e("_Decimal32", Keyword::Decimal32, ALWAYS),
e("_Decimal64", Keyword::Decimal64, ALWAYS),
e("_Decimal128", Keyword::Decimal128, ALWAYS),
e("_Float16", Keyword::Float16, ALWAYS),
e("_Float32", Keyword::Float32, ALWAYS),
e("_Float64", Keyword::Float64, ALWAYS),
e("_Float128", Keyword::Float128, ALWAYS),
e("_Float32x", Keyword::Float32x, ALWAYS),
e("_Float64x", Keyword::Float64x, ALWAYS),
e("_Float128x", Keyword::Float128x, ALWAYS),
e("alignas", Keyword::Alignas, SINCE_C23),
e("alignof", Keyword::Alignof, SINCE_C23),
e("bool", Keyword::Bool, SINCE_C23),
e("constexpr", Keyword::Constexpr, SINCE_C23),
e("false", Keyword::False, SINCE_C23),
e("nullptr", Keyword::Nullptr, SINCE_C23),
e("static_assert", Keyword::StaticAssert, SINCE_C23),
e("thread_local", Keyword::ThreadLocal, SINCE_C23),
e("true", Keyword::True, SINCE_C23),
e("typeof", Keyword::Typeof, SINCE_C23_OR_GNU),
e("typeof_unqual", Keyword::TypeofUnqual, SINCE_C23),
e("asm", Keyword::Asm, GNU_ONLY),
e("__asm", Keyword::Asm, ALWAYS),
e("__asm__", Keyword::Asm, ALWAYS),
e("__alignof", Keyword::GnuAlignof, ALWAYS),
e("__alignof__", Keyword::GnuAlignof, ALWAYS),
e("__attribute", Keyword::Attribute, ALWAYS),
e("__attribute__", Keyword::Attribute, ALWAYS),
e("__auto_type", Keyword::AutoType, ALWAYS),
e("__complex", Keyword::Complex, ALWAYS),
e("__complex__", Keyword::Complex, ALWAYS),
e("__const", Keyword::Const, ALWAYS),
e("__extension__", Keyword::Extension, ALWAYS),
e("__imag", Keyword::Imag, ALWAYS),
e("__imag__", Keyword::Imag, ALWAYS),
e("__inline", Keyword::Inline, ALWAYS),
e("__inline__", Keyword::Inline, ALWAYS),
e("__int128", Keyword::Int128, ALWAYS),
e("__int128_t", Keyword::Int128T, ALWAYS),
e("__label__", Keyword::Label, ALWAYS),
e("__real", Keyword::Real, ALWAYS),
e("__real__", Keyword::Real, ALWAYS),
e("__restrict", Keyword::Restrict, ALWAYS),
e("__restrict__", Keyword::Restrict, ALWAYS),
e("__signed", Keyword::Signed, ALWAYS),
e("__signed__", Keyword::Signed, ALWAYS),
e("__thread", Keyword::ThreadLocal, ALWAYS),
e("__typeof", Keyword::Typeof, ALWAYS),
e("__typeof__", Keyword::Typeof, ALWAYS),
e("__typeof_unqual", Keyword::TypeofUnqual, ALWAYS),
e("__typeof_unqual__", Keyword::TypeofUnqual, ALWAYS),
e("__uint128_t", Keyword::UInt128T, ALWAYS),
e("__volatile", Keyword::Volatile, ALWAYS),
e("__volatile__", Keyword::Volatile, ALWAYS),
e("__builtin_offsetof", Keyword::BuiltinOffsetof, ALWAYS),
e("__builtin_choose_expr", Keyword::BuiltinChooseExpr, ALWAYS),
e("__builtin_types_compatible_p", Keyword::BuiltinTypesCompatibleP, ALWAYS),
e("__builtin_va_arg", Keyword::BuiltinVaArg, ALWAYS),
e("__builtin_va_list", Keyword::BuiltinVaList, ALWAYS),
e("__builtin_va_start", Keyword::BuiltinVaStart, ALWAYS),
e("__builtin_va_end", Keyword::BuiltinVaEnd, ALWAYS),
e("__builtin_va_copy", Keyword::BuiltinVaCopy, ALWAYS),
];
const fn mask(std: Std, gnu: bool) -> u8 {
let dialect = match std {
Std::C89 => C89,
Std::C99 => C99,
Std::C11 => C11,
Std::C17 => C17,
Std::C23 => C23,
};
if gnu { dialect | GNU } else { dialect }
}
#[cfg(test)]
mod tests {
use super::*;
fn build(std: Std, gnu: bool) -> (Keywords, Interner) {
let mut interner = Interner::new();
let keywords = Keywords::new(&mut interner, std, gnu);
(keywords, interner)
}
fn lookup(std: Std, gnu: bool, text: &str) -> Option<Keyword> {
let (keywords, mut interner) = build(std, gnu);
keywords.get(interner.intern(text))
}
#[test]
fn a_word_the_language_has_always_had_is_a_keyword_in_every_dialect() {
for std in [Std::C89, Std::C99, Std::C11, Std::C17, Std::C23] {
for gnu in [false, true] {
assert_eq!(lookup(std, gnu, "int"), Some(Keyword::Int));
assert_eq!(lookup(std, gnu, "sizeof"), Some(Keyword::Sizeof));
assert_eq!(lookup(std, gnu, "_Complex"), Some(Keyword::Complex));
}
}
}
#[test]
fn a_name_a_program_chose_is_never_a_keyword() {
for name in ["x", "intx", "in", "INT", "fortran", "ordinary", "__builtin_expect"] {
assert_eq!(lookup(Std::C23, true, name), None, "{name} is not a keyword");
}
}
#[test]
fn restrict_arrived_in_c99_and_gnu89_did_not_get_it_early() {
assert_eq!(lookup(Std::C89, false, "restrict"), None);
assert_eq!(lookup(Std::C89, true, "restrict"), None);
assert_eq!(lookup(Std::C99, false, "restrict"), Some(Keyword::Restrict));
assert_eq!(lookup(Std::C89, false, "__restrict__"), Some(Keyword::Restrict));
}
#[test]
fn inline_arrived_in_c99_and_gnu89_did_get_it_early() {
assert_eq!(lookup(Std::C89, false, "inline"), None);
assert_eq!(lookup(Std::C89, true, "inline"), Some(Keyword::Inline));
assert_eq!(lookup(Std::C99, false, "inline"), Some(Keyword::Inline));
}
#[test]
fn typeof_is_a_gnu_extension_that_c23_made_standard() {
assert_eq!(lookup(Std::C17, false, "typeof"), None);
assert_eq!(lookup(Std::C17, true, "typeof"), Some(Keyword::Typeof));
assert_eq!(lookup(Std::C23, false, "typeof"), Some(Keyword::Typeof));
assert_eq!(lookup(Std::C17, true, "typeof_unqual"), None);
assert_eq!(lookup(Std::C23, false, "typeof_unqual"), Some(Keyword::TypeofUnqual));
assert_eq!(lookup(Std::C17, false, "__typeof__"), Some(Keyword::Typeof));
}
#[test]
fn asm_is_the_one_word_c23_still_does_not_have() {
assert_eq!(lookup(Std::C23, false, "asm"), None);
assert_eq!(lookup(Std::C23, true, "asm"), Some(Keyword::Asm));
assert_eq!(lookup(Std::C89, false, "__asm__"), Some(Keyword::Asm));
}
#[test]
fn the_c23_words_are_variable_names_in_every_earlier_dialect() {
let added = [
("alignas", Keyword::Alignas),
("alignof", Keyword::Alignof),
("bool", Keyword::Bool),
("constexpr", Keyword::Constexpr),
("false", Keyword::False),
("nullptr", Keyword::Nullptr),
("static_assert", Keyword::StaticAssert),
("thread_local", Keyword::ThreadLocal),
("true", Keyword::True),
];
for (spelling, keyword) in added {
assert_eq!(lookup(Std::C17, true, spelling), None, "{spelling} in gnu17");
assert_eq!(lookup(Std::C23, false, spelling), Some(keyword), "{spelling} in c23");
}
assert_eq!(lookup(Std::C17, false, "_Static_assert"), Some(Keyword::StaticAssert));
assert_eq!(lookup(Std::C23, false, "_Static_assert"), Some(Keyword::StaticAssert));
}
#[test]
fn two_spellings_of_one_thing_are_one_keyword() {
for spelling in ["const", "__const"] {
assert_eq!(lookup(Std::C23, true, spelling), Some(Keyword::Const));
}
for spelling in ["_Thread_local", "thread_local", "__thread"] {
assert_eq!(lookup(Std::C23, true, spelling), Some(Keyword::ThreadLocal));
}
assert_ne!(
lookup(Std::C23, true, "__alignof__"),
lookup(Std::C23, true, "_Alignof"),
"the two alignments are different questions"
);
}
#[test]
fn every_keyword_prints_a_spelling_that_is_that_keyword() {
for entry in KEYWORDS {
let printed = entry.keyword.as_str();
let found = KEYWORDS
.iter()
.find(|other| other.spelling == printed)
.unwrap_or_else(|| panic!("{printed} is not in the table"));
assert_eq!(found.keyword, entry.keyword, "{printed} prints for the wrong keyword");
}
}
#[test]
fn no_spelling_is_in_the_table_twice() {
let mut seen: Vec<&str> = KEYWORDS.iter().map(|entry| entry.spelling).collect();
seen.sort_unstable();
let count = seen.len();
seen.dedup();
assert_eq!(seen.len(), count, "a spelling appears twice in the table");
}
#[test]
fn recognition_does_not_depend_on_what_was_interned_afterwards() {
let (keywords, mut interner) = build(Std::C23, true);
for i in 0..1000 {
let symbol = interner.intern(&format!("name{i}"));
assert_eq!(keywords.get(symbol), None);
}
assert_eq!(keywords.get(interner.intern("while")), Some(Keyword::While));
}
#[test]
#[should_panic(expected = "`static` was interned before the keyword table was built")]
fn an_interner_that_already_has_a_keyword_in_it_is_refused() {
let mut interner = Interner::new();
interner.intern("static");
let _ = Keywords::new(&mut interner, Std::C23, true);
}
#[test]
fn a_lookup_is_a_bounds_check_on_one_run_of_symbols() {
let (keywords, _) = build(Std::C23, true);
assert!(!keywords.is_empty());
assert_eq!(keywords.len(), KEYWORDS.len());
}
}