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}