#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Class {
Operator(&'static str),
Word,
Reject,
}
pub const OPERATOR_ALIASES: &[(char, &str, &str)] = &[
('≠', "!=", "not equal"),
('≤', "<=", "less than or equal"),
('≥', ">=", "greater than or equal"),
('×', "*", "multiplication"),
('÷', "/", "division"),
('−', "-", "minus sign (U+2212, not the ASCII hyphen)"),
('∧', "&&", "logical and"),
('∨', "||", "logical or"),
('¬', "!", "logical not"),
('≡', "==", "identical to"),
];
pub const WELCOME: &[(char, &str)] = &[
(
'λ',
"lambda — the traditional name for an anonymous function",
),
('∀', "for all"),
('∃', "there exists"),
('∈', "element of"),
('∉', "not an element of"),
('∅', "the empty set"),
('∪', "union"),
('∩', "intersection"),
('⊆', "subset of"),
('∘', "function composition"),
('∑', "sum"),
('∏', "product"),
('√', "square root"),
('∞', "infinity"),
('∂', "partial derivative"),
('∇', "gradient / nabla"),
('∫', "integral"),
('→', "maps to / implies"),
('←', "assigned from"),
('↔', "if and only if"),
('⇒', "implies"),
('⊤', "top / true"),
('⊥', "bottom / false"),
('⊢', "proves / entails"),
('π', "pi"),
('α', "alpha"),
('β', "beta"),
('γ', "gamma"),
('δ', "delta"),
('ε', "epsilon"),
('θ', "theta"),
('μ', "mu"),
('σ', "sigma"),
('φ', "phi"),
('ω', "omega"),
('ℕ', "the naturals"),
('ℤ', "the integers"),
('ℚ', "the rationals"),
('ℝ', "the reals"),
('ℂ', "the complex numbers"),
];
#[must_use]
pub fn operator_alias(ch: char) -> Option<&'static str> {
OPERATOR_ALIASES
.iter()
.find(|(c, _, _)| *c == ch)
.map(|(_, op, _)| *op)
}
#[must_use]
pub fn classify(ch: char) -> Class {
if let Some(op) = operator_alias(ch) {
return Class::Operator(op);
}
if ch.is_control() || ch.is_whitespace() {
return Class::Reject;
}
Class::Word
}
#[must_use]
pub fn catalog() -> Vec<(char, String, Class)> {
let mut out: Vec<(char, String, Class)> = OPERATOR_ALIASES
.iter()
.map(|(c, op, why)| (*c, format!("{why} — reads as `{op}`"), classify(*c)))
.collect();
out.extend(
WELCOME
.iter()
.map(|(c, why)| (*c, (*why).to_owned(), classify(*c))),
);
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_alias_lexes_to_the_same_program_as_its_ascii_spelling() {
for (ch, ascii, why) in OPERATOR_ALIASES {
let fancy = crate::parse_program(&format!("def f(a, b)\n a {ch} b\nend"));
let plain = crate::parse_program(&format!("def f(a, b)\n a {ascii} b\nend"));
let plain = plain.unwrap_or_else(|e| panic!("ascii `{ascii}` must parse: {e}"));
let fancy = fancy.unwrap_or_else(|e| panic!("`{ch}` ({why}) must parse: {e}"));
assert_eq!(
format!("{fancy:?}"),
format!("{plain:?}"),
"`{ch}` and `{ascii}` produced DIFFERENT trees — an alias that \
means something other than the operator it looks like is a trap"
);
}
}
#[test]
fn welcomed_symbols_are_usable_as_names() {
for (ch, why) in WELCOME {
assert_eq!(
classify(*ch),
Class::Word,
"`{ch}` ({why}) is in WELCOME but does not classify as part of \
an identifier"
);
let src = format!("def {ch}(n)\n n\nend\n{ch}(1)");
assert!(
crate::parse_program(&src).is_ok(),
"`{ch}` ({why}) is catalogued as a usable name but will not parse"
);
}
}
#[test]
fn the_two_tables_do_not_overlap() {
for (ch, _) in WELCOME {
assert!(
operator_alias(*ch).is_none(),
"`{ch}` appears in BOTH the operator aliases and the welcome \
list — it cannot be an operator and a name at once"
);
}
}
#[test]
fn invisible_characters_are_rejected_rather_than_named() {
assert_eq!(classify('\u{00a0}'), Class::Reject, "no-break space");
assert_eq!(classify('\u{0007}'), Class::Reject, "control character");
}
#[test]
fn an_elsif_arm_is_reachable() {
let src = "def k(a)\n if a < 1\n 1\n elsif a < 5\n 2\n else\n 3\n end\nend";
let forms = crate::parse_program(src).expect("elsif must parse");
let text = format!("{forms:?}");
assert!(
text.matches("Symbol(\"if\")").count() >= 2,
"the elsif arm did not become a nested if — it was swallowed, and \
a swallowed arm returns the else value with no error: {text}"
);
}
#[test]
fn the_catalog_is_whole() {
let c = catalog();
assert_eq!(
c.len(),
OPERATOR_ALIASES.len() + WELCOME.len(),
"catalog() dropped entries — a partial catalog read as a whole one \
is how a reader concludes a character is unsupported"
);
}
}