Skip to main content

rayforce/
expr.rs

1//! Expression AST for the query DSL.
2//!
3//! An [`Expr`] is a column reference, a literal, or an operation applied to
4//! sub-expressions. [`Expr::compile`] lowers it to a `Value` AST (a list whose
5//! head is a name reference to a builtin); [`Expr::execute`] evaluates it.
6//!
7//! Comparisons are methods (`col("p").gt(100)`) because Rust's `==`/`<` must
8//! return `bool`; arithmetic uses real operator overloads (`col("p") + 1`).
9
10use crate::convert::ToValue;
11use crate::ops::Operation;
12use crate::runtime::eval_value;
13use crate::value::Value;
14use crate::Result;
15
16/// A node in the query expression tree.
17#[derive(Clone)]
18pub enum Expr {
19    /// A column / global-env name reference.
20    Col(String),
21    /// A literal value.
22    Lit(Value),
23    /// An operation applied to operands.
24    Op(Operation, Vec<Expr>),
25    /// A call to a named, env-bound function (e.g. a user [`crate::Fn`] lambda)
26    /// applied to operands. Compiles just like [`Expr::Op`] but with an
27    /// arbitrary function name in head position.
28    Call(String, Vec<Expr>),
29}
30
31/// Anything convertible into an [`Expr`] operand: another `Expr`, a literal
32/// scalar, or a [`Value`].
33pub trait IntoExpr {
34    fn into_expr(self) -> Expr;
35}
36
37impl IntoExpr for Expr {
38    fn into_expr(self) -> Expr {
39        self
40    }
41}
42impl IntoExpr for &Expr {
43    fn into_expr(self) -> Expr {
44        self.clone()
45    }
46}
47impl IntoExpr for Value {
48    fn into_expr(self) -> Expr {
49        Expr::Lit(self)
50    }
51}
52
53macro_rules! into_expr_lit {
54    ($($t:ty),*) => {$(
55        impl IntoExpr for $t {
56            fn into_expr(self) -> Expr { Expr::Lit(self.to_value()) }
57        }
58    )*};
59}
60into_expr_lit!(bool, u8, i16, i32, i64, f32, f64);
61
62// In expressions, bare string literals become STRING atoms (matching the
63// Python query compiler): the select/DAG path compares symbol columns against
64// string scalars, whereas a symbol scalar raises a schema error. (This differs
65// from the general `ToValue` path, where `&str` is a symbol.)
66impl IntoExpr for &str {
67    fn into_expr(self) -> Expr {
68        Expr::Lit(Value::string(self))
69    }
70}
71impl IntoExpr for String {
72    fn into_expr(self) -> Expr {
73        Expr::Lit(Value::string(&self))
74    }
75}
76impl<S: AsRef<str>> IntoExpr for crate::Str<S> {
77    fn into_expr(self) -> Expr {
78        Expr::Lit(self.to_value())
79    }
80}
81
82/// A column / global name reference.
83pub fn col(name: impl Into<String>) -> Expr {
84    Expr::Col(name.into())
85}
86
87/// A literal value from any [`ToValue`].
88pub fn lit(v: impl ToValue) -> Expr {
89    Expr::Lit(v.to_value())
90}
91
92impl Expr {
93    /// Build an operation node.
94    pub fn op(op: Operation, operands: Vec<Expr>) -> Expr {
95        Expr::Op(op, operands)
96    }
97
98    fn binary(self, op: Operation, rhs: impl IntoExpr) -> Expr {
99        Expr::Op(op, vec![self, rhs.into_expr()])
100    }
101    fn unary(self, op: Operation) -> Expr {
102        Expr::Op(op, vec![self])
103    }
104
105    // ---- comparisons (methods: `==`/`<` can't return Expr in Rust) ----
106    pub fn eq(self, rhs: impl IntoExpr) -> Expr {
107        self.binary(Operation::Equals, rhs)
108    }
109    pub fn ne(self, rhs: impl IntoExpr) -> Expr {
110        self.binary(Operation::NotEquals, rhs)
111    }
112    pub fn lt(self, rhs: impl IntoExpr) -> Expr {
113        self.binary(Operation::LessThan, rhs)
114    }
115    pub fn le(self, rhs: impl IntoExpr) -> Expr {
116        self.binary(Operation::LessEqual, rhs)
117    }
118    pub fn gt(self, rhs: impl IntoExpr) -> Expr {
119        self.binary(Operation::GreaterThan, rhs)
120    }
121    pub fn ge(self, rhs: impl IntoExpr) -> Expr {
122        self.binary(Operation::GreaterEqual, rhs)
123    }
124    pub fn like(self, pattern: impl IntoExpr) -> Expr {
125        self.binary(Operation::Like, pattern)
126    }
127    pub fn is_in(self, set: impl IntoExpr) -> Expr {
128        self.binary(Operation::In, set)
129    }
130    pub fn within(self, range: impl IntoExpr) -> Expr {
131        self.binary(Operation::Within, range)
132    }
133
134    // ---- logical ----
135    pub fn and(self, rhs: impl IntoExpr) -> Expr {
136        self.binary(Operation::And, rhs)
137    }
138    pub fn or(self, rhs: impl IntoExpr) -> Expr {
139        self.binary(Operation::Or, rhs)
140    }
141
142    // ---- aggregations / reductions ----
143    pub fn sum(self) -> Expr {
144        self.unary(Operation::Sum)
145    }
146    pub fn avg(self) -> Expr {
147        self.unary(Operation::Avg)
148    }
149    pub fn count(self) -> Expr {
150        self.unary(Operation::Count)
151    }
152    pub fn min(self) -> Expr {
153        self.unary(Operation::Min)
154    }
155    pub fn max(self) -> Expr {
156        self.unary(Operation::Max)
157    }
158    pub fn first(self) -> Expr {
159        self.unary(Operation::First)
160    }
161    pub fn last(self) -> Expr {
162        self.unary(Operation::Last)
163    }
164    pub fn median(self) -> Expr {
165        self.unary(Operation::Median)
166    }
167    pub fn deviation(self) -> Expr {
168        self.unary(Operation::Deviation)
169    }
170    pub fn std(self) -> Expr {
171        self.unary(Operation::Stddev)
172    }
173    pub fn distinct(self) -> Expr {
174        self.unary(Operation::Distinct)
175    }
176
177    // ---- math ----
178    pub fn ceil(self) -> Expr {
179        self.unary(Operation::Ceil)
180    }
181    pub fn floor(self) -> Expr {
182        self.unary(Operation::Floor)
183    }
184    pub fn round(self) -> Expr {
185        self.unary(Operation::Round)
186    }
187
188    // ---- collection ----
189    /// Keep elements where `mask` is true.
190    pub fn filter(self, mask: impl IntoExpr) -> Expr {
191        self.binary(Operation::Filter, mask)
192    }
193
194    /// Lower this expression to a `Value` AST.
195    pub fn compile(&self) -> Value {
196        match self {
197            Expr::Col(name) => Value::name_ref(name),
198            Expr::Lit(v) => v.clone(),
199            Expr::Op(op, operands) => {
200                let mut items = Vec::with_capacity(operands.len() + 1);
201                // Head is a name reference to the builtin (as the parser emits);
202                // `ray_eval` resolves and applies it. Aggregations return a lazy
203                // DAG result that the eval entry points materialize.
204                items.push(Value::name_ref(op.as_str()));
205                for o in operands {
206                    items.push(o.compile());
207                }
208                Value::list(&items)
209            }
210            Expr::Call(name, operands) => {
211                // Same shape as `Op`, but the head is a reference to a named
212                // env binding (a user lambda bound by `Fn::apply`).
213                let mut items = Vec::with_capacity(operands.len() + 1);
214                items.push(Value::name_ref(name));
215                for o in operands {
216                    items.push(o.compile());
217                }
218                Value::list(&items)
219            }
220        }
221    }
222
223    /// Compile and evaluate this expression.
224    pub fn execute(&self) -> Result<Value> {
225        eval_value(&self.compile())
226    }
227}
228
229// ---- operator overloads (arithmetic + logical) ----
230
231macro_rules! bin_op {
232    ($trait:ident, $method:ident, $op:expr) => {
233        impl<R: IntoExpr> std::ops::$trait<R> for Expr {
234            type Output = Expr;
235            fn $method(self, rhs: R) -> Expr {
236                Expr::Op($op, vec![self, rhs.into_expr()])
237            }
238        }
239    };
240}
241bin_op!(Add, add, Operation::Add);
242bin_op!(Sub, sub, Operation::Subtract);
243bin_op!(Mul, mul, Operation::Multiply);
244bin_op!(Div, div, Operation::Divide);
245bin_op!(Rem, rem, Operation::Modulo);
246bin_op!(BitAnd, bitand, Operation::And);
247bin_op!(BitOr, bitor, Operation::Or);
248
249impl std::ops::Neg for Expr {
250    type Output = Expr;
251    fn neg(self) -> Expr {
252        self.unary(Operation::Negate)
253    }
254}
255impl std::ops::Not for Expr {
256    type Output = Expr;
257    fn not(self) -> Expr {
258        self.unary(Operation::Not)
259    }
260}
261
262// ---- free-function aggregations: `sum(col("x"))` ----
263
264macro_rules! free_agg {
265    ($name:ident, $op:expr) => {
266        pub fn $name(e: impl IntoExpr) -> Expr {
267            Expr::Op($op, vec![e.into_expr()])
268        }
269    };
270}
271free_agg!(sum, Operation::Sum);
272free_agg!(avg, Operation::Avg);
273free_agg!(count, Operation::Count);
274free_agg!(min, Operation::Min);
275free_agg!(max, Operation::Max);
276free_agg!(first, Operation::First);
277free_agg!(last, Operation::Last);
278free_agg!(median, Operation::Median);
279free_agg!(distinct, Operation::Distinct);