use std::sync::OnceLock;
use crate::form::Form;
use crate::intrinsics::fold;
use crate::reader::read_forms;
const WORDS_TEXT: &str = include_str!("../../data/words.vla");
const NAMES_TEXT: &str = include_str!("../../data/names.vla");
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct FunctionWord {
pub word: String,
pub target: String,
pub takes_of: bool,
}
#[derive(Default)]
struct Tables {
expr_ops: Vec<(String, String)>,
expr_op_words: Vec<String>,
noise_words: Vec<String>,
dropped_words: Vec<String>,
stray_hints: Vec<(String, String)>,
stray_default: String,
number_words: Vec<(String, String)>,
ordinal_words: Vec<(String, String)>,
color_words: Vec<String>,
slot_descs: Vec<(String, String)>,
reserved_names: Vec<String>,
engine_call_names: Vec<String>,
function_words: Vec<FunctionWord>,
function_word_display: Vec<String>,
conditions_grammar_words: Vec<String>,
set_verbs: Vec<String>,
after_value_words: Vec<String>,
words_count: usize,
names_count: usize,
}
fn strings(items: &[Form]) -> Option<Vec<String>> {
items
.iter()
.skip(1)
.map(|f| match f {
Form::Str(s) => Some(s.clone()),
_ => None,
})
.collect()
}
fn pair(items: &[Form]) -> Option<(String, String)> {
let s = strings(items)?;
if s.len() != 2 {
return None;
}
Some((s[0].clone(), s[1].clone()))
}
fn one(items: &[Form]) -> Option<String> {
let s = strings(items)?;
if s.len() != 1 {
return None;
}
Some(s[0].clone())
}
fn parse() -> Tables {
let mut t = Tables::default();
for (text, is_words) in [(WORDS_TEXT, true), (NAMES_TEXT, false)] {
let text = text.replace("\r\n", "\n");
let forms = match read_forms(&text) {
Ok(forms) => forms,
Err(_) => continue,
};
for form in &forms {
let Form::List(l) = form else { continue };
let Some(Form::Sym(head)) = l.items.first() else {
continue;
};
let taken = match head.as_str() {
"expr-op" => pair(&l.items).map(|p| t.expr_ops.push(p)),
"expr-op-word" => one(&l.items).map(|w| t.expr_op_words.push(w)),
"noise-word" => one(&l.items).map(|w| t.noise_words.push(w)),
"dropped-word" => one(&l.items).map(|w| t.dropped_words.push(w)),
"stray-char-hint" => pair(&l.items).map(|p| t.stray_hints.push(p)),
"stray-char-default" => one(&l.items).map(|w| t.stray_default = w),
"number-word" => pair(&l.items).map(|p| t.number_words.push(p)),
"ordinal-word" => pair(&l.items).map(|p| t.ordinal_words.push(p)),
"color-word" => one(&l.items).map(|w| t.color_words.push(w)),
"slot-desc" => pair(&l.items).map(|p| t.slot_descs.push(p)),
"reserved-name" => one(&l.items).map(|w| t.reserved_names.push(w)),
"engine-call-name" => one(&l.items).map(|w| t.engine_call_names.push(w)),
"function-word" => {
let kind = match l.items.get(1) {
Some(Form::Sym(k)) => k.as_str(),
_ => continue,
};
let takes_of = match kind {
"of" => true,
"nullary" => false,
_ => continue,
};
pair(&l.items[1..]).map(|(word, target)| {
t.function_words.push(FunctionWord {
word,
target,
takes_of,
})
})
}
"function-word-display" => one(&l.items).map(|w| t.function_word_display.push(w)),
"conditions-grammar-word" => {
one(&l.items).map(|w| t.conditions_grammar_words.push(w))
}
"set-verb" => one(&l.items).map(|w| t.set_verbs.push(w)),
"after-value-word" => one(&l.items).map(|w| t.after_value_words.push(w)),
_ => None,
};
if taken.is_some() {
if is_words {
t.words_count += 1;
} else {
t.names_count += 1;
}
}
}
}
t
}
fn tables() -> &'static Tables {
static TABLES: OnceLock<Tables> = OnceLock::new();
TABLES.get_or_init(parse)
}
pub fn words_count() -> usize {
tables().words_count
}
pub fn names_count() -> usize {
tables().names_count
}
pub fn expr_op_word(op: &str) -> String {
tables()
.expr_ops
.iter()
.find(|(k, _)| k == op)
.map(|(_, v)| v.clone())
.unwrap_or_default()
}
pub fn is_expr_op_word(w: &str) -> bool {
tables().expr_op_words.iter().any(|x| x == w)
}
pub fn is_noise_word(w: &str) -> bool {
tables().noise_words.iter().any(|x| x == w)
}
pub fn is_dropped_word(w: &str) -> bool {
tables().dropped_words.iter().any(|x| x == w)
}
pub fn stray_char_hint(c: &str) -> String {
let t = tables();
let h = t
.stray_hints
.iter()
.find(|(k, _)| k == c)
.map(|(_, v)| v.as_str())
.unwrap_or(t.stray_default.as_str());
format!(" - {h}")
}
pub fn number_word(w: &str) -> String {
tables()
.number_words
.iter()
.find(|(k, _)| k == w)
.map(|(_, v)| v.clone())
.unwrap_or_else(|| w.to_string())
}
pub fn ordinal_word(w: &str) -> String {
tables()
.ordinal_words
.iter()
.find(|(k, _)| k == w)
.map(|(_, v)| v.clone())
.unwrap_or_default()
}
pub fn is_color_word(s: &str) -> bool {
tables().color_words.iter().any(|x| x == s)
}
pub fn slot_desc(cat: &str) -> String {
tables()
.slot_descs
.iter()
.find(|(k, _)| k == cat)
.map(|(_, v)| v.clone())
.unwrap_or_else(|| cat.to_string())
}
pub fn is_reserved_name(n: &str) -> bool {
tables().reserved_names.contains(&fold(n))
}
pub fn is_engine_call_name(n: &str) -> bool {
tables().engine_call_names.contains(&fold(n))
}
pub fn builtin_function_words() -> &'static [FunctionWord] {
&tables().function_words
}
pub fn function_word_display() -> &'static [String] {
&tables().function_word_display
}
pub fn is_conditions_grammar_word(nm: &str) -> bool {
tables().conditions_grammar_words.iter().any(|x| x == nm)
}
pub fn is_set_verb(t: &str) -> bool {
tables().set_verbs.iter().any(|x| x == t)
}
pub fn is_after_value_word(w: &str) -> bool {
is_expr_op_word(w) || tables().after_value_words.iter().any(|x| x == w)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_two_files_are_read_whole() {
assert!(words_count() >= 113, "{} word entries read", words_count());
assert!(names_count() >= 218, "{} name entries read", names_count());
let t = tables();
assert_eq!(t.expr_ops.len(), 11);
assert_eq!(t.expr_op_words.len(), 7);
assert_eq!(t.noise_words.len(), 4);
assert_eq!(t.dropped_words.len(), 2);
assert_eq!(t.stray_hints.len(), 16);
assert_eq!(t.number_words.len(), 21);
assert_eq!(t.ordinal_words.len(), 20);
assert_eq!(t.color_words.len(), 8);
assert_eq!(t.slot_descs.len(), 23);
assert_eq!(t.engine_call_names.len(), 42);
assert_eq!(t.function_words.len(), 16);
assert_eq!(t.function_word_display.len(), 17);
assert_eq!(t.conditions_grammar_words.len(), 8);
assert_eq!(t.set_verbs.len(), 2);
assert_eq!(t.after_value_words.len(), 12);
assert!(t.reserved_names.len() >= 121);
for w in [
"return", "gosub", "global", "scale", "circle", "decimal", "longlong",
] {
assert!(is_reserved_name(w), "{w} is not reserved");
}
}
#[test]
fn each_table_answers_as_the_vba_does() {
assert_eq!(expr_op_word("<>"), "does not equal");
assert_eq!(expr_op_word("^"), "");
assert!(is_expr_op_word("divided") && !is_expr_op_word("by"));
assert!(is_noise_word("a") && !is_dropped_word("a"));
assert!(is_dropped_word("the") && is_dropped_word("please"));
assert_eq!(stray_char_hint("+"), " - write 'plus' for addition");
assert_eq!(
stray_char_hint("\u{2014}"),
" - use a plain hyphen - (Word may have autocorrected it)"
);
assert_eq!(
stray_char_hint("\\"),
" - if this is a file path, put the WHOLE path in \"quotes\" (a period ends a sentence otherwise)"
);
assert_eq!(
stray_char_hint("@"),
" - remove it, or put it inside \"quotes\" if it is part of a text value"
);
assert_eq!(number_word("twenty"), "20");
assert_eq!(number_word("hundred"), "hundred");
assert_eq!(number_word("One"), "One"); assert_eq!(ordinal_word("twelfth"), "12");
assert_eq!(ordinal_word("zeroth"), "");
assert!(is_color_word("magenta") && !is_color_word("pink"));
assert_eq!(slot_desc("var"), "a name (one word, like total)");
assert_eq!(slot_desc("nothing-of-the-kind"), "nothing-of-the-kind");
assert!(is_reserved_name("Dim") && is_reserved_name("cvar"));
assert!(!is_reserved_name("total"));
assert!(is_engine_call_name("Range") && is_engine_call_name("make-button"));
assert!(!is_engine_call_name("my-range"));
let fw = builtin_function_words();
assert_eq!(fw[0].word, "length");
assert_eq!(fw[0].target, "len");
assert!(fw[0].takes_of);
assert_eq!(fw[14].word, "today");
assert_eq!(fw[14].target, "date");
assert!(!fw[14].takes_of);
assert_eq!(function_word_display()[12], "item <n> of <list>");
assert!(is_conditions_grammar_word("lists") && !is_conditions_grammar_word("has"));
assert!(is_set_verb("are") && !is_set_verb("were"));
assert!(is_after_value_word("percent") && is_after_value_word("plus"));
assert!(!is_after_value_word("total"));
}
}