Skip to main content

hidane_rules/eval/
constraint.rs

1//! What a list knows of a field its query filters on, when the filters do not give its value:
2//! a `constraint`, as the official emulator calls it (`tools/oracle/rules_list.py`).
3//!
4//! A constraint is the field's filters, each a comparator and a value (`n > 0 && n != 3` is
5//! `GT 0`, `NEQ 3`; `not-in [1, 2]` is `NEQ 1`, `NEQ 2`; `!= null` is `NEQ null`;
6//! `array-contains 'a'` is `LIST_CONTAINS 'a'`). A comparison with it is true when one of the
7//! filters alone implies it (`GT 5` implies `> 4`, `>= 5.0` and `!= 1`; `GTE 1` and `LTE 1`
8//! together do not imply `== 1`), and otherwise an error without a message, which a denial
9//! shows as `evaluation error at L5:22`: never false. `!= null` always holds. A float in a
10//! filter does not compare with an int in the rule (`>= 1.5` does not imply `> 1`), though an
11//! int in a filter does compare with a float (`> 5` implies `> 4.5`).
12//!
13//! `x in c` holds when `c` has `LIST_CONTAINS x`, and `c.hasAny(list)` is whether it has one of
14//! the list's items; `c in list` is false, `c is constraint` true, and anything else is the
15//! error any other value of a type without that operation gives.
16
17use std::cmp::Ordering;
18
19use super::value::{Error, R, Val, eq, strict_eq, utf16_cmp};
20use crate::ast::BinaryOp;
21
22/// How a filter of a constraint compares.
23#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
24pub enum Comparator {
25    Eq,
26    Neq,
27    Lt,
28    Lte,
29    Gt,
30    Gte,
31    ListContains,
32}
33
34impl Comparator {
35    /// The official name: `EQ`, `NEQ`, `LT`, `LTE`, `GT`, `GTE` or `LIST_CONTAINS`.
36    pub fn name(self) -> &'static str {
37        match self {
38            Self::Eq => "EQ",
39            Self::Neq => "NEQ",
40            Self::Lt => "LT",
41            Self::Lte => "LTE",
42            Self::Gt => "GT",
43            Self::Gte => "GTE",
44            Self::ListContains => "LIST_CONTAINS",
45        }
46    }
47}
48
49/// One filter of a constraint.
50#[derive(Debug, Clone)]
51pub(crate) struct Simple {
52    pub comparator: Comparator,
53    pub value: Val,
54}
55
56/// `l op r` when either side is a constraint; `None` when neither is, or the operator treats a
57/// constraint like any other value.
58pub(crate) fn binary(op: BinaryOp, l: &Val, r: &Val) -> Option<R> {
59    let comparison = matches!(
60        op,
61        BinaryOp::Eq | BinaryOp::Ne | BinaryOp::Lt | BinaryOp::Le | BinaryOp::Gt | BinaryOp::Ge
62    );
63    Some(match (l, r) {
64        (Val::Constraint(_), Val::Constraint(_)) if comparison || op == BinaryOp::In => {
65            Err(Error::type_error(
66                "constraint",
67                "bool,duration,float,int,latlng,map,null,string,bytes,timestamp,path,list,\
68                 vector,set",
69            ))
70        }
71        (Val::Constraint(c), v) if comparison => implies(c, op, v),
72        (v, Val::Constraint(c)) if comparison => implies(c, flip(op), v),
73        (v, Val::Constraint(c)) if op == BinaryOp::In => {
74            let contains = c
75                .iter()
76                .any(|s| s.comparator == Comparator::ListContains && strict_eq(&s.value, v));
77            if contains {
78                Ok(Val::Bool(true))
79            } else {
80                Err(Error::undecided())
81            }
82        }
83        _ => return None,
84    })
85}
86
87/// `c.hasAny(items)`.
88pub(crate) fn has_any(c: &[Simple], items: &[Val]) -> bool {
89    c.iter().any(|s| {
90        s.comparator == Comparator::ListContains && items.iter().any(|i| strict_eq(&s.value, i))
91    })
92}
93
94/// `v op c` as `c op' v`.
95fn flip(op: BinaryOp) -> BinaryOp {
96    match op {
97        BinaryOp::Lt => BinaryOp::Gt,
98        BinaryOp::Le => BinaryOp::Ge,
99        BinaryOp::Gt => BinaryOp::Lt,
100        BinaryOp::Ge => BinaryOp::Le,
101        other => other,
102    }
103}
104
105/// `c op v`: true if one filter of `c` implies it, else undecided.
106fn implies(c: &[Simple], op: BinaryOp, v: &Val) -> R {
107    let implied = match v {
108        Val::Null => op == BinaryOp::Ne,
109        _ => c.iter().any(|s| s.implies(op, v)),
110    };
111    if implied {
112        Ok(Val::Bool(true))
113    } else {
114        Err(Error::undecided())
115    }
116}
117
118impl Simple {
119    /// Whether every value this filter lets through satisfies `x op v`.
120    fn implies(&self, op: BinaryOp, v: &Val) -> bool {
121        use BinaryOp::{Eq, Ge, Gt, Le, Lt, Ne};
122        use Ordering::{Equal, Greater, Less};
123        // How the filter's value compares with `v`.
124        let Some(order) = relation(&self.value, v) else {
125            return false;
126        };
127        match (self.comparator, order) {
128            (Comparator::Eq, Equal) => matches!(op, Eq | Le | Ge),
129            (Comparator::Eq, Less) => matches!(op, Ne | Lt | Le),
130            (Comparator::Eq, Greater) => matches!(op, Ne | Gt | Ge),
131            (Comparator::Neq, Equal) => op == Ne,
132            (Comparator::Gt, Equal | Greater) | (Comparator::Gte, Greater) => {
133                matches!(op, Gt | Ge | Ne)
134            }
135            (Comparator::Gte, Equal) => op == Ge,
136            (Comparator::Lt, Equal | Less) | (Comparator::Lte, Less) => matches!(op, Lt | Le | Ne),
137            (Comparator::Lte, Equal) => op == Le,
138            _ => false,
139        }
140    }
141}
142
143/// How a filter's value compares with a rule's, if they compare.
144fn relation(filter: &Val, rule: &Val) -> Option<Ordering> {
145    match (filter, rule) {
146        (Val::Int(a), Val::Int(b)) => Some(a.cmp(b)),
147        (Val::Float(a), Val::Float(b)) => a.partial_cmp(b),
148        (Val::Int(a), Val::Float(b)) => (*a as f64).partial_cmp(b),
149        (Val::Float(_), Val::Int(_)) => None,
150        (Val::Str(a), Val::Str(b)) => Some(utf16_cmp(a, b)),
151        (Val::Bytes(a), Val::Bytes(b)) => Some(a.cmp(b)),
152        (Val::Timestamp(s, n), Val::Timestamp(t, m)) => Some((s, n).cmp(&(t, m))),
153        (a, b) if eq(a, b) => Some(Ordering::Equal),
154        _ => None,
155    }
156}