use std::collections::HashSet;
use std::hash::Hash;
use smol_str::SmolStr;
use crate::typed;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Connective {
And,
Or,
}
pub(crate) fn evaluate_predicate<'a, T>(
node: &typed::GlyphsAppPredicate,
glyphs: impl IntoIterator<Item = (T, &'a str)>,
) -> Vec<T>
where
T: Copy + Eq + Hash,
{
let clauses: Vec<(typed::GlyphsAppPredicateOp, SmolStr)> = node
.clauses()
.map(|clause| {
assert_eq!(
clause.attr().text(),
"name",
"non-'name' attributes are rejected by validation"
);
(clause.op(), clause.value().text().into())
})
.collect();
assert!(!clauses.is_empty(), "empty predicates are a parse error");
let connective = node
.connectives()
.map(|conn| match conn {
typed::GlyphsAppPredicateConnective::And(_) => Connective::And,
typed::GlyphsAppPredicateConnective::Or(_) => Connective::Or,
})
.reduce(|prev, this| {
assert_eq!(prev, this, "mixed connectives are rejected by validation");
this
})
.unwrap_or(Connective::And);
let glyphs: Vec<(T, &str)> = glyphs.into_iter().collect();
match connective {
Connective::Or => {
let mut seen = HashSet::new();
let mut out = Vec::new();
for (op, value) in &clauses {
for (id, name) in &glyphs {
if op_matches(op, name, value) && seen.insert(*id) {
out.push(*id);
}
}
}
out
}
Connective::And => glyphs
.iter()
.filter(|(_, name)| {
clauses
.iter()
.all(|(op, value)| op_matches(op, name, value))
})
.map(|(id, _)| *id)
.collect(),
}
}
fn op_matches(op: &typed::GlyphsAppPredicateOp, name: &str, value: &str) -> bool {
use typed::GlyphsAppPredicateOp as Op;
match op {
Op::BeginsWith(_) => name.starts_with(value),
Op::EndsWith(_) => name.ends_with(value),
Op::Contains(_) => name.contains(value),
Op::Eq(_) => name == value,
Op::Ne(_) => name != value,
Op::Lt(_) => name < value,
Op::Le(_) => name <= value,
Op::Gt(_) => name > value,
Op::Ge(_) => name >= value,
Op::Like(_) | Op::Matches(_) => {
unreachable!("like/matches are rejected by validation")
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::token_tree::typed::AstNode;
fn parse_predicate(inner: &str) -> typed::GlyphsAppPredicate {
let src = format!("$[{inner}]");
let (node, diags, err_str) = crate::parse::grammar::debug_parse_output(&src, |parser| {
crate::parse::grammar::eat_glyphs_predicate(parser, crate::TokenSet::EMPTY);
});
assert!(
!diags.iter().any(|diag| diag.is_error()),
"`{inner}` produced parse errors that would stop real compilation, so \
this evaluator test would be exercising a recovered parse:\n{err_str}"
);
typed::GlyphsAppPredicate::cast(&node)
.unwrap_or_else(|| panic!("`{inner}` did not parse as a predicate"))
}
fn eval(inner: &str, glyphs: &[(u16, &str)]) -> Vec<u16> {
evaluate_predicate(
&parse_predicate(inner),
glyphs.iter().map(|(id, name)| (*id, *name)),
)
}
fn names<'a>(inner: &str, glyphs: &[(u16, &'a str)]) -> Vec<&'a str> {
let ids = eval(inner, glyphs);
ids.iter()
.map(|id| glyphs.iter().find(|(g, _)| g == id).unwrap().1)
.collect()
}
fn sample() -> Vec<(u16, &'static str)> {
[
"A",
"A.sc",
"B",
"behDotless-ar.init",
"behDotless-ar.init.fbeh2",
"behDotless-ar.medi",
"meem-ar.init",
"meem-ar.medi",
"ss01.a",
"x.ss01",
]
.iter()
.enumerate()
.map(|(i, n)| (i as u16, *n))
.collect()
}
#[test]
fn endswith_single_quote() {
let glyphs = sample();
assert_eq!(names("name endswith 'ss01'", &glyphs), vec!["x.ss01"]);
}
#[test]
fn contains_double_quote() {
let glyphs = sample();
assert_eq!(
names("name contains \"meem-ar\"", &glyphs),
vec!["meem-ar.init", "meem-ar.medi"]
);
}
#[test]
fn beginswith() {
let glyphs = sample();
assert_eq!(
names("name beginswith \"behDotless\"", &glyphs),
vec![
"behDotless-ar.init",
"behDotless-ar.init.fbeh2",
"behDotless-ar.medi"
]
);
}
#[test]
fn flat_and_with_not_equal() {
let glyphs = sample();
assert_eq!(
names(
"name contains \"behDotless-ar.init\" and name != \"behDotless-ar.init.fbeh2\"",
&glyphs
),
vec!["behDotless-ar.init"]
);
}
#[test]
fn flat_or() {
let glyphs = sample();
assert_eq!(
names(
"name contains \"meem-ar.init\" or name contains \"meem-ar.medi\"",
&glyphs
),
vec!["meem-ar.init", "meem-ar.medi"]
);
}
#[test]
fn or_preserves_clause_order_not_glyph_order() {
let glyphs = [(0u16, "x.medi"), (1u16, "x.init")];
assert_eq!(
eval(
"name endswith \".init\" or name endswith \".medi\"",
&glyphs
),
vec![1, 0]
);
}
#[test]
fn or_dedups() {
let glyphs = [(0u16, "ab"), (1u16, "ba")];
assert_eq!(
eval("name contains \"a\" or name contains \"b\"", &glyphs),
vec![0, 1]
);
}
#[test]
fn empty_result_is_empty() {
let glyphs = sample();
assert!(eval("name endswith \"zzzz\"", &glyphs).is_empty());
}
#[test]
fn operator_keywords_case_insensitive() {
let glyphs = sample();
assert_eq!(names("name ENDSWITH 'ss01'", &glyphs), vec!["x.ss01"]);
assert_eq!(
names(
"name contains \"meem-ar.init\" OR name contains \"meem-ar.medi\"",
&glyphs
),
vec!["meem-ar.init", "meem-ar.medi"]
);
}
#[test]
fn value_case_sensitive() {
let glyphs = [(0u16, "A.sc"), (1u16, "a.sc")];
assert_eq!(eval("name beginswith \"A\"", &glyphs), vec![0]);
}
#[test]
fn symbolic_aliases() {
let glyphs = [(0u16, "a"), (1u16, "b")];
assert_eq!(eval("name = \"a\"", &glyphs), vec![0]);
assert_eq!(eval("name == \"a\"", &glyphs), vec![0]);
assert_eq!(eval("name != \"a\"", &glyphs), vec![1]);
assert_eq!(eval("name <> \"a\"", &glyphs), vec![1]);
}
#[test]
fn relational_operators_are_lexicographic() {
let glyphs = [(0u16, "a"), (1u16, "m"), (2u16, "z")];
assert_eq!(eval("name < \"m\"", &glyphs), vec![0]);
assert_eq!(eval("name <= \"m\"", &glyphs), vec![0, 1]);
assert_eq!(eval("name > \"m\"", &glyphs), vec![2]);
assert_eq!(eval("name >= \"m\"", &glyphs), vec![1, 2]);
}
#[test]
fn quoted_boolean_word_is_a_plain_string() {
let glyphs = [(0u16, "noon"), (1u16, "a")];
assert_eq!(eval("name == \"noon\"", &glyphs), vec![0]);
}
}