src/filter/dsl/eval/eval.rs

//! Direct interpretation of a [`CompiledFilter`] against an [`EvalContext`].
//!
//! The compiled form is a flat vector of "nodes" indexed by compile-time
//! offsets. That keeps the hot path branchy but cache-friendly.

use crate::filter::dsl::ast::Cmp;
use crate::filter::dsl::compile::{CompiledFilter, Node};

use super::context::EvalContext;

pub fn evaluate(filter: &CompiledFilter, ctx: &EvalContext) -> bool {
    eval_node(filter, 0, ctx)
}

fn eval_node(filter: &CompiledFilter, index: usize, ctx: &EvalContext) -> bool {
    match &filter.nodes[index] {
        Node::And { left, right } => {
            eval_node(filter, *left, ctx) && eval_node(filter, *right, ctx)
        }
        Node::Or { left, right } => {
            eval_node(filter, *left, ctx) || eval_node(filter, *right, ctx)
        }
        Node::Not { inner } => !eval_node(filter, *inner, ctx),
        Node::CompareStr { field, op, value } => {
            compare_strings(ctx.get(field), *op, value)
        }
        Node::CompareInt { field, op, value } => match ctx.get_int(field) {
            Some(lhs) => compare_ints(lhs, *op, *value),
            None => false,
        },
        Node::Match { field, regex } => regex.is_match(ctx.get(field)),
    }
}

fn compare_strings(lhs: &str, op: Cmp, rhs: &str) -> bool {
    match op {
        Cmp::Eq => lhs == rhs,
        Cmp::Ne => lhs != rhs,
        Cmp::Lt => lhs < rhs,
        Cmp::Le => lhs <= rhs,
        Cmp::Gt => lhs > rhs,
        Cmp::Ge => lhs >= rhs,
        Cmp::Match => false,
    }
}

fn compare_ints(lhs: i64, op: Cmp, rhs: i64) -> bool {
    match op {
        Cmp::Eq => lhs == rhs,
        Cmp::Ne => lhs != rhs,
        Cmp::Lt => lhs < rhs,
        Cmp::Le => lhs <= rhs,
        Cmp::Gt => lhs > rhs,
        Cmp::Ge => lhs >= rhs,
        Cmp::Match => false,
    }
}