wirefilter/
filter.rs

1use execution_context::ExecutionContext;
2use failure::Fail;
3use scheme::Scheme;
4
5/// An error that occurs if filter and provided [`ExecutionContext`] have
6/// different [schemes](struct@Scheme).
7#[derive(Debug, PartialEq, Fail)]
8#[fail(display = "execution context doesn't match the scheme with which filter was parsed")]
9pub struct SchemeMismatchError;
10
11// Each AST expression node gets compiled into CompiledExpr. Therefore, Filter
12// essentialy is a public API facade for a tree of CompiledExprs. When filter
13// gets executed it calls `execute` method on its root expression which then
14// under the hood propagates field values to its leafs by recursively calling
15// their `execute` methods and aggregating results into a single boolean value
16// as recursion unwinds.
17pub(crate) struct CompiledExpr<'s>(Box<dyn 's + Fn(&ExecutionContext<'s>) -> bool>);
18
19impl<'s> CompiledExpr<'s> {
20    /// Creates a compiled expression IR from a generic closure.
21    pub(crate) fn new(closure: impl 's + Fn(&ExecutionContext<'s>) -> bool) -> Self {
22        CompiledExpr(Box::new(closure))
23    }
24
25    /// Executes a filter against a provided context with values.
26    pub fn execute(&self, ctx: &ExecutionContext<'s>) -> bool {
27        self.0(ctx)
28    }
29}
30
31/// An IR for a compiled filter expression.
32///
33/// Currently it works by creating and combining boxed untyped closures and
34/// performing indirect calls between them, which is fairly cheap, but,
35/// surely, not as fast as an inline code with real JIT compilers.
36///
37/// On the other hand, it's much less risky than allocating, trusting and
38/// executing code at runtime, because all the code being executed is
39/// already statically generated and verified by the Rust compiler and only the
40/// data differs. For the same reason, our "compilation" times are much better
41/// than with a full JIT compiler as well.
42///
43/// In the future the underlying representation might change, but for now it
44/// provides the best trade-off between safety and performance of compilation
45/// and execution.
46pub struct Filter<'s> {
47    root_expr: CompiledExpr<'s>,
48    scheme: &'s Scheme,
49}
50
51impl<'s> Filter<'s> {
52    /// Creates a compiled expression IR from a generic closure.
53    pub(crate) fn new(root_expr: CompiledExpr<'s>, scheme: &'s Scheme) -> Self {
54        Filter { root_expr, scheme }
55    }
56
57    /// Executes a filter against a provided context with values.
58    pub fn execute(&self, ctx: &ExecutionContext<'s>) -> Result<bool, SchemeMismatchError> {
59        if self.scheme == ctx.scheme() {
60            Ok(self.root_expr.execute(ctx))
61        } else {
62            Err(SchemeMismatchError)
63        }
64    }
65}
66
67#[cfg(test)]
68mod tests {
69    use super::SchemeMismatchError;
70    use execution_context::ExecutionContext;
71
72    #[test]
73    fn test_scheme_mismatch() {
74        let scheme1 = Scheme! { foo: Int };
75        let scheme2 = Scheme! { foo: Int, bar: Int };
76        let filter = scheme1.parse("foo == 42").unwrap().compile();
77        let ctx = ExecutionContext::new(&scheme2);
78
79        assert_eq!(filter.execute(&ctx), Err(SchemeMismatchError));
80    }
81}