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 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 Coalesce,
102 Min,
104 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}