use crate::vocab::Symbol;
mod generated;
pub use generated::{
Keyword, MYSQL_FUNCTION_ONLY_KEYWORDS, MYSQL_RESERVED_KEYWORDS, MYSQL_TYPE_FUNC_NAME_KEYWORDS,
POSTGRES_AS_LABEL_KEYWORDS, POSTGRES_COL_NAME_KEYWORDS, POSTGRES_RESERVED_KEYWORDS,
POSTGRES_TYPE_FUNC_NAME_KEYWORDS, lookup_keyword,
};
impl Keyword {
pub fn symbol(self) -> Symbol {
Symbol::new(self as u32 + 1).expect("keyword symbols are one-based")
}
}
const _: () = {
let mut index = 0;
while index < Keyword::ALL.len() {
assert!(
Keyword::ALL[index] as usize == index,
"Keyword::ALL must list every variant in discriminant order; symbol() and \
the resolver's reverse lookup index by `self as u32`",
);
index += 1;
}
};
const KEYWORD_WORDS: usize = Keyword::ALL.len().div_ceil(u64::BITS as usize);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct KeywordSet([u64; KEYWORD_WORDS]);
impl KeywordSet {
pub const EMPTY: Self = Self([0; KEYWORD_WORDS]);
pub const fn from_keywords(keywords: &[Keyword]) -> Self {
let mut words = [0; KEYWORD_WORDS];
let mut index = 0;
while index < keywords.len() {
let (word, bit) = keyword_slot(keywords[index] as usize);
words[word] |= bit;
index += 1;
}
Self(words)
}
pub const fn union(self, other: Self) -> Self {
let mut words = self.0;
let mut index = 0;
while index < KEYWORD_WORDS {
words[index] |= other.0[index];
index += 1;
}
Self(words)
}
pub const fn difference(self, other: Self) -> Self {
let mut words = self.0;
let mut index = 0;
while index < KEYWORD_WORDS {
words[index] &= !other.0[index];
index += 1;
}
Self(words)
}
pub const fn contains(self, keyword: Keyword) -> bool {
let (word, bit) = keyword_slot(keyword as usize);
self.0[word] & bit != 0
}
}
const fn keyword_slot(discriminant: usize) -> (usize, u64) {
let bits = u64::BITS as usize;
(discriminant / bits, 1_u64 << (discriminant % bits))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn lookup_keyword_is_case_insensitive_and_alloc_free() {
assert_eq!(lookup_keyword("select"), Some(Keyword::Select));
assert_eq!(lookup_keyword("SeLeCt"), Some(Keyword::Select));
assert_eq!(lookup_keyword("selector"), None);
assert_eq!(lookup_keyword("café"), None);
}
#[test]
fn lookup_table_covers_every_keyword() {
for keyword in Keyword::ALL {
assert_eq!(lookup_keyword(keyword.as_str()), Some(keyword));
}
}
fn reference_lookup(word: &str) -> Option<Keyword> {
let lowered = word.to_ascii_lowercase();
Keyword::ALL
.into_iter()
.find(|&keyword| keyword.as_str() == lowered)
}
fn alternating_case(spelling: &str) -> String {
spelling
.bytes()
.enumerate()
.map(|(index, byte)| {
if index % 2 == 0 {
byte.to_ascii_uppercase() as char
} else {
byte.to_ascii_lowercase() as char
}
})
.collect()
}
#[test]
fn packed_lookup_agrees_with_reference_over_keywords_and_non_keywords() {
const NON_KEYWORDS: &[&str] = &[
"user_id",
"created_at",
"customer_email",
"order_total",
"line_item",
"txn_ref",
"qty_on_hand",
"unit_price_usd",
"is_active",
"uuid_pk",
"foo",
"bar",
"baz",
"xyzzy",
"col_1",
"",
"_",
"select_",
"_select",
"selectt",
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
"café",
"naïve",
"señor",
];
let mut probes: Vec<String> = Vec::new();
for keyword in Keyword::ALL {
let spelling = keyword.as_str();
probes.push(spelling.to_string());
probes.push(spelling.to_ascii_uppercase());
probes.push(alternating_case(spelling));
probes.push(format!("{spelling}z"));
probes.push(format!("z{spelling}"));
if spelling.len() > 1 {
probes.push(spelling[..spelling.len() - 1].to_string());
}
}
probes.extend(NON_KEYWORDS.iter().map(|word| (*word).to_string()));
for probe in &probes {
assert_eq!(
lookup_keyword(probe),
reference_lookup(probe),
"packed lookup disagrees with the reference scan on {probe:?}",
);
}
}
#[test]
fn keyword_symbols_are_fixed_low_slots() {
assert_eq!(Keyword::A.symbol().as_u32(), 1);
assert_eq!(Keyword::A as usize, 0);
let last = *Keyword::ALL.last().expect("non-empty inventory");
assert_eq!(last.symbol().as_u32(), Keyword::ALL.len() as u32);
for keyword in Keyword::ALL {
assert_eq!(keyword.symbol().as_u32(), keyword as u32 + 1);
}
}
#[test]
fn keyword_set_round_trips_every_keyword() {
let all = KeywordSet::from_keywords(&Keyword::ALL);
for keyword in Keyword::ALL {
assert!(all.contains(keyword), "{keyword:?} should be present");
assert!(
!KeywordSet::EMPTY.contains(keyword),
"{keyword:?} should be absent from the empty set",
);
}
}
#[test]
fn keyword_set_union_is_membership_union() {
let left = KeywordSet::from_keywords(&[Keyword::Select, Keyword::From]);
let right = KeywordSet::from_keywords(&[Keyword::From, Keyword::Where]);
let union = left.union(right);
for keyword in [Keyword::Select, Keyword::From, Keyword::Where] {
assert!(
union.contains(keyword),
"{keyword:?} should be in the union"
);
}
assert!(
!union.contains(Keyword::Join),
"Join was in neither operand"
);
}
#[test]
fn keyword_bitset_addresses_slots_across_word_boundaries() {
assert_eq!(keyword_slot(0), (0, 1));
assert_eq!(keyword_slot(63), (0, 1 << 63));
assert_eq!(keyword_slot(64), (1, 1));
assert_eq!(keyword_slot(127), (1, 1 << 63));
assert_eq!(keyword_slot(128), (2, 1));
let mut words = [0_u64; 3];
let present = [0_usize, 1, 63, 64, 65, 127, 129];
for &discriminant in &present {
let (word, bit) = keyword_slot(discriminant);
words[word] |= bit;
}
for discriminant in 0..130_usize {
let (word, bit) = keyword_slot(discriminant);
assert_eq!(
words[word] & bit != 0,
present.contains(&discriminant),
"slot {discriminant}",
);
}
}
}