Skip to main content

radiate_expr/
ops.rs

1use radiate_utils::{DataType, SmallStr};
2#[cfg(feature = "serde")]
3use serde::{Deserialize, Serialize};
4use std::fmt::Debug;
5
6#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
7#[derive(Clone, PartialEq)]
8pub enum ScheduleOp {
9    Interval {
10        count: usize,
11        limit: usize,
12    },
13    Duration {
14        #[cfg_attr(feature = "serde", serde(skip))]
15        last: Option<std::time::Instant>,
16        interval: std::time::Duration,
17    },
18    Warmup {
19        period: usize,
20        current: usize,
21    },
22}
23
24impl Debug for ScheduleOp {
25    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
26        match self {
27            ScheduleOp::Interval { count, limit } => {
28                write!(f, "Interval {{ count: {}, limit: {} }}", count, limit)
29            }
30            ScheduleOp::Duration { last, interval } => {
31                write!(
32                    f,
33                    "Duration {{ last: {:?}, interval: {:?} }}",
34                    last, interval
35                )
36            }
37            ScheduleOp::Warmup { period, current } => {
38                write!(f, "Warmup {{ period: {}, current: {} }}", period, current)
39            }
40        }
41    }
42}
43
44#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
45#[derive(Clone, Debug, PartialEq)]
46pub enum UnaryOp {
47    Not,
48    Neg,
49    Abs,
50    Cast(DataType),
51    Debug,
52    /// Fused affine: `scale * child + bias`. Replaces the `.mul(lit).add(lit)`
53    /// pattern with a single node. Chains collapse via [`fuse_affine`].
54    Affine {
55        scale: f32,
56        bias: f32,
57    },
58    Stagnation {
59        epsilon: f32,
60        last_value: Option<f32>,
61        count: u32,
62    },
63}
64
65#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
66#[derive(Clone, Debug, PartialEq)]
67pub enum RollupOp {
68    First,
69    Last,
70    Mean,
71    StdDev,
72    Min,
73    Max,
74    Sum,
75    Var,
76    Skew,
77    Count,
78    Unique,
79    Slope,
80    Quantile(f32),
81}
82
83#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
84#[derive(Clone, Copy, Debug, PartialEq)]
85pub enum BinaryOp {
86    Add,
87    Sub,
88    Mul,
89    Div,
90    And,
91    Or,
92    Lt,
93    Lte,
94    Gt,
95    Gte,
96    Eq,
97    Ne,
98    Mod,
99    Pow,
100    /// Returns lhs if finite, otherwise rhs. Treats Null, NaN, ±Inf as fallback triggers.
101    Coalesce,
102    /// Elementwise min of two numeric values. NaN-on-one-side returns the other.
103    Min,
104    /// Elementwise max of two numeric values. NaN-on-one-side returns the other.
105    Max,
106}
107
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109#[derive(Clone, Copy, Debug, PartialEq)]
110pub enum TrinaryOp {
111    If,
112    Clamp,
113}
114
115#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
116#[derive(Clone, PartialEq)]
117pub enum SelectOp {
118    Identity,
119    Index(usize),
120    Range(usize, usize),
121    Field(SmallStr),
122    Nested {
123        parent: Box<SelectOp>,
124        child: Box<SelectOp>,
125    },
126}
127
128impl From<usize> for SelectOp {
129    fn from(idx: usize) -> Self {
130        SelectOp::Index(idx)
131    }
132}
133
134impl From<std::ops::Range<usize>> for SelectOp {
135    fn from(range: std::ops::Range<usize>) -> Self {
136        SelectOp::Range(range.start, range.end)
137    }
138}
139
140impl From<SmallStr> for SelectOp {
141    fn from(field: SmallStr) -> Self {
142        SelectOp::Field(field)
143    }
144}
145
146impl From<(SmallStr, SmallStr)> for SelectOp {
147    fn from((parent, child): (SmallStr, SmallStr)) -> Self {
148        SelectOp::Nested {
149            parent: Box::new(SelectOp::Field(parent)),
150            child: Box::new(SelectOp::Field(child)),
151        }
152    }
153}
154
155impl Debug for SelectOp {
156    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157        match self {
158            SelectOp::Identity => write!(f, "Identity"),
159            SelectOp::Index(idx) => write!(f, "Index({})", idx),
160            SelectOp::Range(start, end) => write!(f, "Range({},{})", start, end),
161            SelectOp::Field(field) => write!(f, "Field({})", field),
162            SelectOp::Nested { parent, child } => {
163                write!(f, "Nested({:?}, {:?})", parent, child)
164            }
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172    use crate::{Expr, ExprResult, ProjectExpr, ops::SelectOp};
173    use radiate_error::RadiateError;
174    use radiate_utils::AnyValue;
175
176    struct NullMetrics;
177    impl<'a> ProjectExpr<'a> for NullMetrics {
178        fn select(&'a self, _: &SelectOp) -> Result<AnyValue<'a>, RadiateError> {
179            Ok(AnyValue::Null)
180        }
181    }
182
183    fn eval(mut expr: Expr) -> ExprResult<'static> {
184        let metrics = NullMetrics;
185        expr.evaluate(&metrics).map(|v| v.into_static())
186    }
187
188    #[test]
189    fn not_negates_bool() {
190        assert_eq!(eval(Expr::lit(true).not()).unwrap(), AnyValue::Bool(false));
191    }
192
193    #[test]
194    fn not_on_non_bool_errors() {
195        assert!(eval(Expr::lit(5.0_f32).not()).is_err());
196    }
197
198    #[test]
199    fn neg_and_abs_on_numeric() {
200        assert_eq!(
201            eval(Expr::lit(3.0_f32).neg()).unwrap(),
202            AnyValue::Float32(-3.0)
203        );
204        assert_eq!(
205            eval(Expr::lit(-3.0_f32).abs()).unwrap(),
206            AnyValue::Float32(3.0)
207        );
208    }
209
210    #[test]
211    fn cast_changes_dtype() {
212        assert_eq!(
213            eval(Expr::lit(3.0_f32).cast(DataType::UInt64)).unwrap(),
214            AnyValue::UInt64(3)
215        );
216    }
217
218    #[test]
219    fn stagnation_counts_consecutive_small_changes() {
220        let mut expr = Expr::lit(1.0_f32).stagnation(0.1);
221        let metrics = NullMetrics;
222        assert_eq!(
223            expr.evaluate(&metrics).unwrap().into_static(),
224            AnyValue::UInt32(0)
225        );
226        assert_eq!(
227            expr.evaluate(&metrics).unwrap().into_static(),
228            AnyValue::UInt32(1)
229        );
230        assert_eq!(
231            expr.evaluate(&metrics).unwrap().into_static(),
232            AnyValue::UInt32(2)
233        );
234    }
235
236    #[test]
237    fn binary_arithmetic_and_comparison() {
238        assert_eq!(
239            eval(Expr::lit(2_i32).add(3_i32)).unwrap(),
240            AnyValue::Int32(5)
241        );
242        assert_eq!(
243            eval(Expr::lit(2_i32).lt(3_i32)).unwrap(),
244            AnyValue::Bool(true)
245        );
246        assert_eq!(
247            eval(Expr::lit(2_i32).eq(2_i32)).unwrap(),
248            AnyValue::Bool(true)
249        );
250    }
251
252    #[test]
253    fn coalesce_falls_back_to_rhs_on_nan() {
254        let result = eval(Expr::lit(f32::NAN).coalesce(Expr::lit(7.0_f32))).unwrap();
255        assert_eq!(result, AnyValue::Float32(7.0));
256    }
257
258    #[test]
259    fn min_with_and_max_with() {
260        assert_eq!(
261            eval(Expr::lit(2.0_f32).min_with(5.0_f32)).unwrap(),
262            AnyValue::Float32(2.0)
263        );
264        assert_eq!(
265            eval(Expr::lit(2.0_f32).max_with(5.0_f32)).unwrap(),
266            AnyValue::Float32(5.0)
267        );
268    }
269
270    #[test]
271    fn trinary_if_picks_correct_branch() {
272        let then_expr = Expr::when(Expr::lit(true)).then(1_i32).otherwise(2_i32);
273        assert_eq!(eval(then_expr).unwrap(), AnyValue::Int32(1));
274
275        let else_expr = Expr::when(Expr::lit(false)).then(1_i32).otherwise(2_i32);
276        assert_eq!(eval(else_expr).unwrap(), AnyValue::Int32(2));
277    }
278
279    #[test]
280    fn trinary_if_requires_bool_condition() {
281        let expr = Expr::when(Expr::lit(1_i32)).then(1_i32).otherwise(2_i32);
282        assert!(eval(expr).is_err());
283    }
284
285    #[test]
286    fn clamp_bounds_finite_value() {
287        let expr = Expr::lit(15.0_f32).clamp(0.0_f32, 10.0_f32);
288        assert_eq!(eval(expr).unwrap(), AnyValue::Float32(10.0));
289    }
290
291    #[test]
292    fn clamp_falls_back_to_floor_on_nan() {
293        let expr = Expr::lit(f32::NAN).clamp(1.0_f32, 10.0_f32);
294        assert_eq!(eval(expr).unwrap(), AnyValue::Float32(1.0));
295    }
296}