pub const ANY: &str = "*";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SlotYield {
Keywords(&'static [&'static str]),
KeywordsAndTables(&'static [&'static str]),
Tables,
Analyzers,
Expression,
}
const BASE: &[&str] = &["ROOT", "NAMESPACE", "NS", "DATABASE", "DB"];
const INFO_TARGETS: &[&str] = &[
"ROOT",
"NAMESPACE",
"NS",
"DATABASE",
"DB",
"TABLE",
"TB",
"USER",
"INDEX",
];
const DEFINE_FORMS: &[&str] = &[
"NAMESPACE",
"DATABASE",
"FUNCTION",
"USER",
"PARAM",
"TABLE",
"API",
"EVENT",
"FIELD",
"INDEX",
"ANALYZER",
"ACCESS",
"CONFIG",
"BUCKET",
"SEQUENCE",
"MODULE",
];
const REMOVE_FORMS: &[&str] = &[
"NAMESPACE",
"DATABASE",
"TABLE",
"FUNCTION",
"MODULE",
"ACCESS",
"USER",
"PARAM",
"EVENT",
"FIELD",
"INDEX",
"ANALYZER",
"SEQUENCE",
"API",
"BUCKET",
"CONFIG",
];
const ALTER_FORMS: &[&str] = &[
"SYSTEM",
"NAMESPACE",
"DATABASE",
"TABLE",
"EVENT",
"INDEX",
"FIELD",
"PARAM",
"SEQUENCE",
"BUCKET",
"ANALYZER",
"FUNCTION",
"USER",
"ACCESS",
"CONFIG",
"API",
"MODULE",
];
const DEFINE_TABLE_CLAUSES: &[&str] = &[
"COMMENT",
"DROP",
"TYPE",
"SCHEMALESS",
"SCHEMAFULL",
"PERMISSIONS",
"CHANGEFEED",
"AS",
"GRAPHQL_ALIAS",
"GRAPHQL_DEPRECATED",
];
const DEFINE_FIELD_CLAUSES: &[&str] = &[
"TYPE",
"FLEXIBLE",
"READONLY",
"VALUE",
"ASSERT",
"DEFAULT",
"PERMISSIONS",
"COMMENT",
"REFERENCE",
"COMPUTED",
"GRAPHQL_ALIAS",
"GRAPHQL_DEPRECATED",
];
const DEFINE_EVENT_CLAUSES: &[&str] = &["WHEN", "THEN", "COMMENT", "ASYNC", "RETRY", "MAXDEPTH"];
const DEFINE_INDEX_CLAUSES: &[&str] = &[
"FIELDS",
"COLUMNS",
"UNIQUE",
"COUNT",
"FULLTEXT",
"HNSW",
"DISKANN",
"CONCURRENTLY",
"COMMENT",
];
const TABLE_TYPES: &[&str] = &["NORMAL", "RELATION", "ANY"];
const PERMISSIONS_HEADS: &[&str] = &["NONE", "FULL", "FOR"];
pub const OFFERS_THE_GRAMMAR_CANNOT_PARSE: &[&str] = &[
"GRAPHQL_ALIAS",
"GRAPHQL_DEPRECATED",
"SYSTEM",
"DISKANN",
"RETRY",
"MAXDEPTH",
];
struct HeadRule {
prefix: &'static [&'static str],
yields: SlotYield,
}
const HEAD_RULES: &[HeadRule] = &[
HeadRule {
prefix: &["INFO"],
yields: SlotYield::Keywords(&["FOR"]),
},
HeadRule {
prefix: &["INFO", "FOR"],
yields: SlotYield::Keywords(INFO_TARGETS),
},
HeadRule {
prefix: &["INFO", "FOR", "TABLE"],
yields: SlotYield::Tables,
},
HeadRule {
prefix: &["INFO", "FOR", "TB"],
yields: SlotYield::Tables,
},
HeadRule {
prefix: &["INFO", "FOR", "ROOT"],
yields: SlotYield::Keywords(&["VERSION", "STRUCTURE"]),
},
HeadRule {
prefix: &["INFO", "FOR", "NAMESPACE"],
yields: SlotYield::Keywords(&["VERSION", "STRUCTURE"]),
},
HeadRule {
prefix: &["INFO", "FOR", "NS"],
yields: SlotYield::Keywords(&["VERSION", "STRUCTURE"]),
},
HeadRule {
prefix: &["INFO", "FOR", "DATABASE"],
yields: SlotYield::Keywords(&["VERSION", "STRUCTURE"]),
},
HeadRule {
prefix: &["INFO", "FOR", "DB"],
yields: SlotYield::Keywords(&["VERSION", "STRUCTURE"]),
},
HeadRule {
prefix: &["INFO", "FOR", "TABLE", ANY],
yields: SlotYield::Keywords(&["VERSION", "STRUCTURE"]),
},
HeadRule {
prefix: &["INFO", "FOR", "TB", ANY],
yields: SlotYield::Keywords(&["VERSION", "STRUCTURE"]),
},
HeadRule {
prefix: &["INFO", "FOR", "INDEX", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["INFO", "FOR", "INDEX", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["INFO", "FOR", "USER", ANY],
yields: SlotYield::Keywords(&["ON", "STRUCTURE"]),
},
HeadRule {
prefix: &["INFO", "FOR", "USER", ANY, "ON"],
yields: SlotYield::Keywords(BASE),
},
HeadRule {
prefix: &["USE"],
yields: SlotYield::Keywords(&["NAMESPACE", "NS", "DATABASE", "DB", "DEFAULT"]),
},
HeadRule {
prefix: &["USE", "NAMESPACE", ANY],
yields: SlotYield::Keywords(&["DATABASE", "DB"]),
},
HeadRule {
prefix: &["USE", "NS", ANY],
yields: SlotYield::Keywords(&["DATABASE", "DB"]),
},
HeadRule {
prefix: &["SHOW"],
yields: SlotYield::Keywords(&["CHANGES"]),
},
HeadRule {
prefix: &["SHOW", "CHANGES"],
yields: SlotYield::Keywords(&["FOR"]),
},
HeadRule {
prefix: &["SHOW", "CHANGES", "FOR"],
yields: SlotYield::Keywords(&["TABLE", "DATABASE"]),
},
HeadRule {
prefix: &["SHOW", "CHANGES", "FOR", "TABLE"],
yields: SlotYield::Tables,
},
HeadRule {
prefix: &["SHOW", "CHANGES", "FOR", "TABLE", ANY],
yields: SlotYield::Keywords(&["SINCE"]),
},
HeadRule {
prefix: &["SHOW", "CHANGES", "FOR", "DATABASE"],
yields: SlotYield::Keywords(&["SINCE"]),
},
HeadRule {
prefix: &["REBUILD"],
yields: SlotYield::Keywords(&["INDEX"]),
},
HeadRule {
prefix: &["REBUILD", "INDEX"],
yields: SlotYield::Keywords(&["IF"]),
},
HeadRule {
prefix: &["REBUILD", "INDEX", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["REBUILD", "INDEX", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["DEFINE"],
yields: SlotYield::Keywords(DEFINE_FORMS),
},
HeadRule {
prefix: &["DEFINE", "TABLE"],
yields: SlotYield::KeywordsAndTables(&["IF", "OVERWRITE"]),
},
HeadRule {
prefix: &["DEFINE", "TABLE", ANY],
yields: SlotYield::Keywords(DEFINE_TABLE_CLAUSES),
},
HeadRule {
prefix: &["DEFINE", "TABLE", ANY, "TYPE"],
yields: SlotYield::Keywords(TABLE_TYPES),
},
HeadRule {
prefix: &["DEFINE", "TABLE", ANY, "PERMISSIONS"],
yields: SlotYield::Keywords(PERMISSIONS_HEADS),
},
HeadRule {
prefix: &["DEFINE", "FIELD"],
yields: SlotYield::Keywords(&["IF", "OVERWRITE"]),
},
HeadRule {
prefix: &["DEFINE", "FIELD", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["DEFINE", "FIELD", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["DEFINE", "FIELD", ANY, "ON", ANY],
yields: SlotYield::Keywords(DEFINE_FIELD_CLAUSES),
},
HeadRule {
prefix: &["DEFINE", "FIELD", ANY, "ON", "TABLE", ANY],
yields: SlotYield::Keywords(DEFINE_FIELD_CLAUSES),
},
HeadRule {
prefix: &["DEFINE", "EVENT"],
yields: SlotYield::Keywords(&["IF", "OVERWRITE"]),
},
HeadRule {
prefix: &["DEFINE", "EVENT", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["DEFINE", "EVENT", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["DEFINE", "EVENT", ANY, "ON", ANY],
yields: SlotYield::Keywords(DEFINE_EVENT_CLAUSES),
},
HeadRule {
prefix: &["DEFINE", "EVENT", ANY, "ON", "TABLE", ANY],
yields: SlotYield::Keywords(DEFINE_EVENT_CLAUSES),
},
HeadRule {
prefix: &["DEFINE", "INDEX"],
yields: SlotYield::Keywords(&["IF", "OVERWRITE"]),
},
HeadRule {
prefix: &["DEFINE", "INDEX", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["DEFINE", "INDEX", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["DEFINE", "INDEX", ANY, "ON", ANY],
yields: SlotYield::Keywords(DEFINE_INDEX_CLAUSES),
},
HeadRule {
prefix: &["DEFINE", "INDEX", ANY, "ON", "TABLE", ANY],
yields: SlotYield::Keywords(DEFINE_INDEX_CLAUSES),
},
HeadRule {
prefix: &["DEFINE", "USER"],
yields: SlotYield::Keywords(&["IF", "OVERWRITE"]),
},
HeadRule {
prefix: &["DEFINE", "USER", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["DEFINE", "USER", ANY, "ON"],
yields: SlotYield::Keywords(BASE),
},
HeadRule {
prefix: &["DEFINE", "ACCESS"],
yields: SlotYield::Keywords(&["IF", "OVERWRITE"]),
},
HeadRule {
prefix: &["DEFINE", "ACCESS", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["DEFINE", "ACCESS", ANY, "ON"],
yields: SlotYield::Keywords(BASE),
},
HeadRule {
prefix: &["DEFINE", "CONFIG"],
yields: SlotYield::Keywords(&["IF", "OVERWRITE", "API", "GRAPHQL", "DEFAULT"]),
},
HeadRule {
prefix: &["REMOVE"],
yields: SlotYield::Keywords(REMOVE_FORMS),
},
HeadRule {
prefix: &["REMOVE", "NAMESPACE"],
yields: SlotYield::Keywords(&["AND", "IF"]),
},
HeadRule {
prefix: &["REMOVE", "DATABASE"],
yields: SlotYield::Keywords(&["AND", "IF"]),
},
HeadRule {
prefix: &["REMOVE", "TABLE"],
yields: SlotYield::KeywordsAndTables(&["AND", "IF"]),
},
HeadRule {
prefix: &["REMOVE", "NAMESPACE", "AND"],
yields: SlotYield::Keywords(&["EXPUNGE"]),
},
HeadRule {
prefix: &["REMOVE", "DATABASE", "AND"],
yields: SlotYield::Keywords(&["EXPUNGE"]),
},
HeadRule {
prefix: &["REMOVE", "TABLE", "AND"],
yields: SlotYield::Keywords(&["EXPUNGE"]),
},
HeadRule {
prefix: &["REMOVE", "FIELD", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["REMOVE", "FIELD", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["REMOVE", "EVENT", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["REMOVE", "EVENT", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["REMOVE", "INDEX", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["REMOVE", "INDEX", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["REMOVE", "USER", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["REMOVE", "USER", ANY, "ON"],
yields: SlotYield::Keywords(BASE),
},
HeadRule {
prefix: &["REMOVE", "ACCESS", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["REMOVE", "ACCESS", ANY, "ON"],
yields: SlotYield::Keywords(BASE),
},
HeadRule {
prefix: &["REMOVE", "ANALYZER"],
yields: SlotYield::Analyzers,
},
HeadRule {
prefix: &["ALTER", "ANALYZER"],
yields: SlotYield::Analyzers,
},
HeadRule {
prefix: &["REMOVE", "CONFIG"],
yields: SlotYield::Keywords(&["IF", "GRAPHQL", "API", "DEFAULT"]),
},
HeadRule {
prefix: &["ALTER"],
yields: SlotYield::Keywords(ALTER_FORMS),
},
HeadRule {
prefix: &["ALTER", "TABLE"],
yields: SlotYield::KeywordsAndTables(&["IF"]),
},
HeadRule {
prefix: &["ALTER", "FIELD", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["ALTER", "FIELD", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["ALTER", "EVENT", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["ALTER", "EVENT", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["ALTER", "INDEX", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["ALTER", "INDEX", ANY, "ON"],
yields: SlotYield::KeywordsAndTables(&["TABLE"]),
},
HeadRule {
prefix: &["ALTER", "USER", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["ALTER", "USER", ANY, "ON"],
yields: SlotYield::Keywords(BASE),
},
HeadRule {
prefix: &["ALTER", "ACCESS", ANY],
yields: SlotYield::Keywords(&["ON"]),
},
HeadRule {
prefix: &["ALTER", "ACCESS", ANY, "ON"],
yields: SlotYield::Keywords(BASE),
},
HeadRule {
prefix: &["ACCESS", ANY],
yields: SlotYield::Keywords(&["ON", "GRANT", "SHOW", "REVOKE", "PURGE"]),
},
HeadRule {
prefix: &["ACCESS", ANY, "ON"],
yields: SlotYield::Keywords(BASE),
},
HeadRule {
prefix: &["ACCESS", ANY, "GRANT"],
yields: SlotYield::Keywords(&["FOR"]),
},
HeadRule {
prefix: &["ACCESS", ANY, "GRANT", "FOR"],
yields: SlotYield::Keywords(&["USER", "RECORD"]),
},
HeadRule {
prefix: &["ACCESS", ANY, "SHOW"],
yields: SlotYield::Keywords(&["ALL", "GRANT", "WHERE"]),
},
HeadRule {
prefix: &["ACCESS", ANY, "REVOKE"],
yields: SlotYield::Keywords(&["ALL", "GRANT", "WHERE"]),
},
HeadRule {
prefix: &["ACCESS", ANY, "PURGE"],
yields: SlotYield::Keywords(&["EXPIRED", "REVOKED"]),
},
HeadRule {
prefix: &["DEFINE", ANY, "IF"],
yields: SlotYield::Keywords(&["NOT", "EXISTS"]),
},
HeadRule {
prefix: &["DEFINE", ANY, "IF", "NOT"],
yields: SlotYield::Keywords(&["EXISTS"]),
},
HeadRule {
prefix: &["REMOVE", ANY, "IF"],
yields: SlotYield::Keywords(&["EXISTS"]),
},
HeadRule {
prefix: &["ALTER", ANY, "IF"],
yields: SlotYield::Keywords(&["EXISTS"]),
},
HeadRule {
prefix: &["REBUILD", "INDEX", "IF"],
yields: SlotYield::Keywords(&["EXISTS"]),
},
];
const TAIL_RULES: &[(&[&str], SlotYield)] = &[(&["FULLTEXT", "ANALYZER"], SlotYield::Analyzers)];
pub fn head_slot(words: &[&str]) -> SlotYield {
let mut best: Option<(u32, SlotYield)> = None;
for rule in HEAD_RULES {
if !matches_prefix(rule.prefix, words) {
continue;
}
let score = specificity(rule.prefix);
if best.is_none_or(|(highest, _)| score > highest) {
best = Some((score, rule.yields));
}
}
if let Some((_, yields)) = best {
return yields;
}
for (tail, yields) in TAIL_RULES {
if words.len() >= tail.len() && matches_prefix(tail, &words[words.len() - tail.len()..]) {
return *yields;
}
}
SlotYield::Expression
}
fn specificity(prefix: &[&str]) -> u32 {
debug_assert!(prefix.len() <= 32, "a rule prefix must fit the score mask");
prefix
.iter()
.enumerate()
.filter(|(_, word)| **word != ANY)
.fold(0, |mask, (index, _)| mask | 1 << index)
}
fn matches_prefix(prefix: &[&str], words: &[&str]) -> bool {
prefix.len() == words.len()
&& prefix
.iter()
.zip(words)
.all(|(expected, actual)| *expected == ANY || expected.eq_ignore_ascii_case(actual))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grammar::KEYWORDS;
fn keywords(words: &[&str]) -> Vec<&'static str> {
match head_slot(words) {
SlotYield::Keywords(list) | SlotYield::KeywordsAndTables(list) => list.to_vec(),
other => panic!("expected a keyword slot for {words:?}, got {other:?}"),
}
}
#[test]
fn info_for_offers_exactly_the_nine_engine_targets() {
assert_eq!(
keywords(&["INFO", "FOR"]),
vec![
"ROOT",
"NAMESPACE",
"NS",
"DATABASE",
"DB",
"TABLE",
"TB",
"USER",
"INDEX"
]
);
}
#[test]
fn info_for_does_not_offer_the_scope_targets_surrealdb_dropped() {
let offered = keywords(&["INFO", "FOR"]);
assert!(
!offered.contains(&"SC") && !offered.contains(&"SCOPE"),
"SurrealDB 3.x has no INFO FOR SCOPE arm (stmt/mod.rs:417-476), got {offered:?}"
);
}
#[test]
fn info_alone_offers_only_for() {
assert_eq!(keywords(&["INFO"]), vec!["FOR"]);
}
#[test]
fn info_for_table_offers_tables() {
assert_eq!(head_slot(&["INFO", "FOR", "TABLE"]), SlotYield::Tables);
assert_eq!(head_slot(&["INFO", "FOR", "TB"]), SlotYield::Tables);
}
#[test]
fn matching_ignores_keyword_case() {
assert_eq!(head_slot(&["info", "for"]), head_slot(&["INFO", "FOR"]));
assert_eq!(head_slot(&["Info", "For", "Table"]), SlotYield::Tables);
}
#[test]
fn a_spelled_word_beats_a_wildcard_at_the_same_slot() {
assert_eq!(keywords(&["REBUILD", "INDEX", "IF"]), vec!["EXISTS"]);
}
#[test]
fn the_slot_nearest_the_cursor_breaks_a_wildcard_tie() {
assert_eq!(keywords(&["DEFINE", "TABLE", "IF"]), vec!["NOT", "EXISTS"]);
assert!(keywords(&["DEFINE", "TABLE", "person"]).contains(&"SCHEMAFULL"));
}
#[test]
fn use_offers_the_four_scope_keywords_and_default() {
assert_eq!(
keywords(&["USE"]),
vec!["NAMESPACE", "NS", "DATABASE", "DB", "DEFAULT"]
);
}
#[test]
fn define_offers_the_sixteen_sub_forms() {
let offered = keywords(&["DEFINE"]);
assert_eq!(offered.len(), 16, "got {offered:?}");
assert!(offered.contains(&"SEQUENCE") && offered.contains(&"MODULE"));
}
#[test]
fn define_does_not_offer_the_sub_forms_surrealdb_removed() {
let offered = keywords(&["DEFINE"]);
for gone in ["TOKEN", "SCOPE", "MODEL"] {
assert!(!offered.contains(&gone), "3.x has no DEFINE {gone} arm");
}
}
#[test]
fn alter_offers_the_seventeen_sub_forms() {
assert_eq!(keywords(&["ALTER"]).len(), 17);
}
#[test]
fn remove_offers_the_sixteen_sub_forms() {
assert_eq!(keywords(&["REMOVE"]).len(), 16);
}
#[test]
fn define_table_clause_bag_uses_as_not_view() {
let offered = keywords(&["DEFINE", "TABLE", "person"]);
assert!(offered.contains(&"AS"), "the view clause opens with AS");
assert!(
!offered.contains(&"VIEW"),
"DEFINE TABLE t VIEW … is a parse error (define.rs:710-722)"
);
}
#[test]
fn define_table_type_offers_only_the_three_table_types() {
assert_eq!(
keywords(&["DEFINE", "TABLE", "person", "TYPE"]),
vec!["NORMAL", "RELATION", "ANY"]
);
}
#[test]
fn remove_namespace_offers_and_before_if() {
assert_eq!(keywords(&["REMOVE", "NAMESPACE"]), vec!["AND", "IF"]);
assert_eq!(keywords(&["REMOVE", "NAMESPACE", "AND"]), vec!["EXPUNGE"]);
}
#[test]
fn on_clause_offers_the_three_bases_with_both_spellings() {
assert_eq!(
keywords(&["DEFINE", "USER", "bob", "ON"]),
vec!["ROOT", "NAMESPACE", "NS", "DATABASE", "DB"]
);
}
#[test]
fn field_and_event_on_slots_offer_the_table_keyword_and_table_names() {
for words in [
vec!["DEFINE", "FIELD", "name", "ON"],
vec!["DEFINE", "EVENT", "audit", "ON"],
vec!["DEFINE", "INDEX", "idx", "ON"],
vec!["REMOVE", "FIELD", "name", "ON"],
vec!["ALTER", "FIELD", "name", "ON"],
] {
assert_eq!(
head_slot(&words),
SlotYield::KeywordsAndTables(&["TABLE"]),
"{words:?} must offer TABLE and the known tables"
);
}
}
#[test]
fn the_field_clause_bag_survives_the_optional_table_keyword() {
let without = head_slot(&["DEFINE", "FIELD", "name", "ON", "person"]);
let with = head_slot(&["DEFINE", "FIELD", "name", "ON", "TABLE", "person"]);
assert_eq!(without, with, "`ON TABLE t` and `ON t` are the same slot");
}
#[test]
fn access_purge_offers_only_the_two_grant_states() {
assert_eq!(
keywords(&["ACCESS", "api", "PURGE"]),
vec!["EXPIRED", "REVOKED"]
);
}
#[test]
fn an_unmodelled_position_falls_back_to_the_current_behaviour() {
for words in [
vec![],
vec!["SELECT"],
vec!["SELECT", "*", "FROM", "person"],
vec![
"SELECT", "*", "FROM", "person", "WHERE", "age", ">", "3", "AND",
],
vec!["CREATE"],
vec!["LET"],
vec!["DEFINE", "FIELD", "name", "ON", "person", "DEFAULT"],
vec!["INFO", "FOR", "USER"],
vec!["REMOVE", "USER"],
vec!["SLEEP"],
vec!["KILL"],
] {
assert_eq!(
head_slot(&words),
SlotYield::Expression,
"{words:?} must not be narrowed"
);
}
}
#[test]
fn no_slot_ever_yields_an_empty_keyword_list() {
for rule in HEAD_RULES {
match rule.yields {
SlotYield::Keywords(list) | SlotYield::KeywordsAndTables(list) => assert!(
!list.is_empty(),
"{:?} yields an empty keyword list",
rule.prefix
),
SlotYield::Tables | SlotYield::Analyzers | SlotYield::Expression => {}
}
}
}
#[test]
fn every_rule_prefix_starts_with_a_statement_keyword() {
for rule in HEAD_RULES {
let first = rule.prefix.first().expect("a rule needs a prefix");
assert_ne!(*first, ANY, "a rule must not open with a wildcard");
assert!(
KEYWORDS.contains(first),
"`{first}` is not a grammar keyword"
);
}
}
#[test]
fn offers_outside_the_grammar_are_declared() {
for rule in HEAD_RULES {
let offered = match rule.yields {
SlotYield::Keywords(list) | SlotYield::KeywordsAndTables(list) => list,
SlotYield::Tables | SlotYield::Analyzers | SlotYield::Expression => continue,
};
for word in offered {
assert!(
KEYWORDS.contains(word) || OFFERS_THE_GRAMMAR_CANNOT_PARSE.contains(word),
"`{word}` (offered at {:?}) is neither a grammar keyword nor a \
declared gap — fix the spelling or add it to \
OFFERS_THE_GRAMMAR_CANNOT_PARSE",
rule.prefix
);
}
}
}
#[test]
fn every_declared_grammar_gap_is_really_absent_from_the_grammar() {
for word in OFFERS_THE_GRAMMAR_CANNOT_PARSE {
assert!(
!KEYWORDS.contains(word),
"`{word}` is in the grammar now — remove it from \
OFFERS_THE_GRAMMAR_CANNOT_PARSE"
);
}
}
}