Skip to main content

radiate_core/stats/expression/
mod.rs

1mod aggregate;
2mod builder;
3mod compile;
4mod logical;
5mod ops;
6mod query;
7mod schedule;
8mod select;
9mod traits;
10
11pub use query::MetricQuery;
12pub use select::{MetricField, MetricKind, SelectExpr};
13pub(crate) use traits::ExprResult;
14pub use traits::{Evaluate, ExprSelector};
15
16use aggregate::AggExpr;
17use logical::When;
18use ops::{BinaryExpr, TrinaryExpr, UnaryExpr};
19use radiate_utils::{AnyValue, SmallStr};
20use schedule::{EveryState, ScheduleExpr};
21#[cfg(feature = "serde")]
22use serde::{Deserialize, Serialize};
23
24#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
25#[derive(Clone, Debug, PartialEq)]
26pub enum Expr {
27    Literal(AnyValue<'static>),
28    Selector(SelectExpr),
29    Aggregate(AggExpr),
30    Schedule(ScheduleExpr),
31    Binary(BinaryExpr),
32    Unary(UnaryExpr),
33    Trinary(TrinaryExpr),
34}
35
36impl<T> Evaluate<T> for Expr
37where
38    T: ExprSelector,
39{
40    fn eval<'a>(&'a mut self, metrics: &T) -> ExprResult<'a> {
41        match self {
42            Expr::Literal(value) => Ok(value.clone()),
43            Expr::Selector(selector) => selector.eval(metrics),
44            Expr::Aggregate(child) => child.eval(metrics),
45            Expr::Trinary(child) => child.eval(metrics),
46            Expr::Binary(child) => child.eval(metrics),
47            Expr::Unary(child) => child.eval(metrics),
48            Expr::Schedule(child) => child.eval(metrics),
49        }
50    }
51}
52
53impl Expr {
54    /// Recursively clears state in stateful operators: rolling-window buffers
55    /// in `Aggregate`/`Buffer` nodes and counters in `Schedule::Every`. Children
56    /// of binary/unary/trinary nodes are also visited. Leaf nodes (literals,
57    /// selectors) are unaffected.
58    ///
59    /// Use after an engine restart or whenever the controller should "forget"
60    /// accumulated history.
61    pub fn reset(&mut self) {
62        match self {
63            Expr::Literal(_) | Expr::Selector(_) => {}
64            Expr::Aggregate(a) => a.reset(),
65            Expr::Schedule(ScheduleExpr::Every(s)) => s.reset(),
66            Expr::Binary(b) => {
67                b.lhs.reset();
68                b.rhs.reset();
69            }
70            Expr::Unary(u) => {
71                u.reset();
72            }
73            Expr::Trinary(t) => {
74                t.first.reset();
75                t.second.reset();
76                t.third.reset();
77            }
78        }
79    }
80
81    pub fn lit(value: impl Into<AnyValue<'static>>) -> Expr {
82        Expr::Literal(value.into())
83    }
84
85    pub fn select(name: impl Into<SmallStr>) -> Expr {
86        Expr::Selector(SelectExpr::new(name))
87    }
88
89    pub fn when(cond: impl Into<Expr>) -> When {
90        When::new(cond.into())
91    }
92
93    pub fn every(interval: usize) -> When {
94        When::new(Expr::Schedule(ScheduleExpr::Every(EveryState::new(
95            interval,
96        ))))
97    }
98
99    pub fn identity() -> Expr {
100        Expr::Selector(SelectExpr {
101            metric: None,
102            field: MetricField::LastValue,
103            kind: MetricKind::Value,
104        })
105    }
106}
107
108#[cfg(test)]
109mod tests {
110    use super::ops::UnaryOp;
111    use super::{Evaluate, Expr};
112    use crate::MetricSet;
113    use radiate_utils::{AnyValue, DataType};
114
115    fn is_fused_affine(e: &Expr) -> bool {
116        matches!(e, Expr::Unary(u) if matches!(u.op, UnaryOp::Affine { .. }))
117    }
118
119    fn metrics() -> MetricSet {
120        MetricSet::default()
121    }
122
123    fn f32_val(v: AnyValue<'_>) -> f32 {
124        v.extract::<f32>().expect("expected f32")
125    }
126
127    fn bool_val(v: AnyValue<'_>) -> bool {
128        match v {
129            AnyValue::Bool(b) => b,
130            other => panic!("expected bool, got {other:?}"),
131        }
132    }
133
134    // ---- Literals ----
135
136    #[test]
137    fn lit_evaluates_to_its_value() {
138        let mut e = Expr::lit(3.14f32);
139        assert!((f32_val(e.eval(&metrics()).unwrap()) - 3.14).abs() < 1e-6);
140    }
141
142    #[test]
143    fn lit_ignores_input() {
144        let mut e = Expr::lit(42.0f32);
145        // Same result regardless of what the input is
146        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 42.0);
147        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 42.0);
148    }
149
150    // ---- Unary ops ----
151
152    #[test]
153    fn neg_negates_numeric() {
154        let mut e = Expr::lit(5.0f32).neg();
155        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), -5.0);
156    }
157
158    #[test]
159    fn abs_returns_magnitude() {
160        let mut e = Expr::lit(-7.0f32).abs();
161        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 7.0);
162    }
163
164    #[test]
165    fn not_inverts_bool() {
166        let mut t = Expr::Literal(AnyValue::Bool(true)).not();
167        let mut f = Expr::Literal(AnyValue::Bool(false)).not();
168        assert!(!bool_val(t.eval(&metrics()).unwrap()));
169        assert!(bool_val(f.eval(&metrics()).unwrap()));
170    }
171
172    #[test]
173    fn not_on_non_bool_errors() {
174        let mut e = Expr::lit(1.0f32).not();
175        assert!(e.eval(&metrics()).is_err());
176    }
177
178    #[test]
179    fn cast_f32_to_i32_truncates() {
180        let mut e = Expr::lit(3.9f32).cast(DataType::Int32);
181        let result = e.eval(&metrics()).unwrap();
182        assert_eq!(result.extract::<i32>(), Some(3));
183    }
184
185    // ---- Arithmetic binary ops ----
186
187    #[test]
188    fn add_two_literals() {
189        let mut e = Expr::lit(2.0f32).add(Expr::lit(3.0f32));
190        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 5.0);
191    }
192
193    #[test]
194    fn sub_two_literals() {
195        let mut e = Expr::lit(10.0f32).sub(Expr::lit(3.0f32));
196        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 7.0);
197    }
198
199    #[test]
200    fn mul_two_literals() {
201        let mut e = Expr::lit(4.0f32) * 2.5f32;
202        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 10.0);
203    }
204
205    #[test]
206    fn div_two_literals() {
207        let mut e = Expr::lit(9.0f32).div(Expr::lit(3.0f32));
208        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 3.0);
209    }
210
211    #[test]
212    fn pow_two_literals() {
213        let mut e = Expr::lit(2.0f32).pow(Expr::lit(8.0f32));
214        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 256.0);
215    }
216
217    // ---- Operator overloads ----
218
219    #[test]
220    fn add_operator_overload() {
221        let mut e = Expr::from(3.0f32) + Expr::from(4.0f32);
222        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 7.0);
223    }
224
225    #[test]
226    fn neg_operator_overload() {
227        let mut e = -Expr::from(5.0f32);
228        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), -5.0);
229    }
230
231    #[test]
232    fn not_operator_overload() {
233        let mut e = !Expr::Literal(AnyValue::Bool(true));
234        assert!(!bool_val(e.eval(&metrics()).unwrap()));
235    }
236
237    // ---- Comparison ops ----
238
239    #[test]
240    fn lt_lte_gt_gte_correct() {
241        let five = || Expr::lit(5.0f32);
242        let ten = || Expr::lit(10.0f32);
243        let input = &metrics();
244
245        assert!(bool_val(five().lt(ten()).eval(input).unwrap()));
246        assert!(!bool_val(ten().lt(five()).eval(input).unwrap()));
247        assert!(bool_val(five().lte(five()).eval(input).unwrap()));
248        assert!(bool_val(ten().gt(five()).eval(input).unwrap()));
249        assert!(bool_val(ten().gte(ten()).eval(input).unwrap()));
250        assert!(!bool_val(five().gte(ten()).eval(input).unwrap()));
251    }
252
253    #[test]
254    fn eq_and_ne_correct() {
255        let input = &metrics();
256        assert!(bool_val(
257            Expr::lit(5.0f32).eq(Expr::lit(5.0f32)).eval(input).unwrap()
258        ));
259        assert!(!bool_val(
260            Expr::lit(5.0f32).eq(Expr::lit(6.0f32)).eval(input).unwrap()
261        ));
262        assert!(bool_val(
263            Expr::lit(5.0f32).ne(Expr::lit(6.0f32)).eval(input).unwrap()
264        ));
265    }
266
267    #[test]
268    fn between_is_inclusive_on_both_ends() {
269        let input = &metrics();
270        let range = || (Expr::lit(1.0f32), Expr::lit(10.0f32));
271
272        let (lo, hi) = range();
273        assert!(bool_val(
274            Expr::lit(5.0f32).between(lo, hi).eval(input).unwrap()
275        ));
276
277        let (lo, hi) = range();
278        assert!(bool_val(
279            Expr::lit(1.0f32).between(lo, hi).eval(input).unwrap()
280        ));
281
282        let (lo, hi) = range();
283        assert!(bool_val(
284            Expr::lit(10.0f32).between(lo, hi).eval(input).unwrap()
285        ));
286
287        let (lo, hi) = range();
288        assert!(!bool_val(
289            Expr::lit(0.0f32).between(lo, hi).eval(input).unwrap()
290        ));
291    }
292
293    // ---- Logical ops ----
294
295    #[test]
296    fn and_or_short_circuit_values() {
297        let input = &metrics();
298        let t = || Expr::Literal(AnyValue::Bool(true));
299        let f = || Expr::Literal(AnyValue::Bool(false));
300
301        assert!(!bool_val(t().and(f()).eval(input).unwrap()));
302        assert!(bool_val(t().and(t()).eval(input).unwrap()));
303        assert!(bool_val(f().or(t()).eval(input).unwrap()));
304        assert!(!bool_val(f().or(f()).eval(input).unwrap()));
305    }
306
307    // ---- When / then / otherwise ----
308
309    #[test]
310    fn when_selects_then_branch_on_true() {
311        let mut e = Expr::when(Expr::Literal(AnyValue::Bool(true)))
312            .then(Expr::lit(1.0f32))
313            .otherwise(Expr::lit(2.0f32));
314        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 1.0);
315    }
316
317    #[test]
318    fn when_selects_otherwise_branch_on_false() {
319        let mut e = Expr::when(Expr::Literal(AnyValue::Bool(false)))
320            .then(Expr::lit(1.0f32))
321            .otherwise(Expr::lit(2.0f32));
322        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 2.0);
323    }
324
325    #[test]
326    fn when_condition_can_be_a_comparison() {
327        let mut e = Expr::when(Expr::lit(5.0f32).gt(Expr::lit(3.0f32)))
328            .then(Expr::lit(100.0f32))
329            .otherwise(Expr::lit(0.0f32));
330        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 100.0);
331    }
332
333    // ---- Clamp ----
334
335    #[test]
336    fn clamp_below_min_returns_min() {
337        let mut e = Expr::lit(-5.0f32).clamp(Expr::lit(0.0f32), Expr::lit(1.0f32));
338        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
339    }
340
341    #[test]
342    fn clamp_above_max_returns_max() {
343        let mut e = Expr::lit(10.0f32).clamp(Expr::lit(0.0f32), Expr::lit(1.0f32));
344        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 1.0);
345    }
346
347    #[test]
348    fn clamp_within_range_unchanged() {
349        let mut e = Expr::lit(0.5f32).clamp(Expr::lit(0.0f32), Expr::lit(1.0f32));
350        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.5);
351    }
352
353    #[test]
354    fn clamp_null_input_returns_min() {
355        let mut e = Expr::Literal(AnyValue::Null).clamp(Expr::lit(0.05f32), Expr::lit(2.0f32));
356        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.05);
357    }
358
359    #[test]
360    fn clamp_nan_input_returns_min() {
361        let mut e = Expr::lit(f32::NAN).clamp(Expr::lit(0.05f32), Expr::lit(2.0f32));
362        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.05);
363    }
364
365    #[test]
366    fn clamp_pos_inf_input_returns_min() {
367        let mut e = Expr::lit(f32::INFINITY).clamp(Expr::lit(0.05f32), Expr::lit(2.0f32));
368        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.05);
369    }
370
371    #[test]
372    fn clamp_neg_inf_input_returns_min() {
373        let mut e = Expr::lit(f32::NEG_INFINITY).clamp(Expr::lit(0.05f32), Expr::lit(2.0f32));
374        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.05);
375    }
376
377    #[test]
378    fn clamp_missing_bounds_errors() {
379        let mut e = Expr::lit(0.5f32).clamp(Expr::Literal(AnyValue::Null), Expr::lit(2.0f32));
380        assert!(e.eval(&metrics()).is_err());
381    }
382
383    // ---- or_else (Coalesce) ----
384
385    #[test]
386    fn or_else_finite_passes_through() {
387        let mut e = Expr::lit(3.0f32).or_else(Expr::lit(99.0f32));
388        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 3.0);
389    }
390
391    #[test]
392    fn or_else_null_falls_back() {
393        let mut e = Expr::Literal(AnyValue::Null).or_else(Expr::lit(99.0f32));
394        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 99.0);
395    }
396
397    #[test]
398    fn or_else_nan_falls_back() {
399        let mut e = Expr::lit(f32::NAN).or_else(Expr::lit(99.0f32));
400        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 99.0);
401    }
402
403    #[test]
404    fn or_else_inf_falls_back() {
405        let mut e = Expr::lit(f32::INFINITY).or_else(Expr::lit(99.0f32));
406        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 99.0);
407    }
408
409    #[test]
410    fn or_else_neg_inf_falls_back() {
411        let mut e = Expr::lit(f32::NEG_INFINITY).or_else(Expr::lit(99.0f32));
412        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 99.0);
413    }
414
415    #[test]
416    fn or_else_chains_through_bad_values() {
417        let mut e = Expr::Literal(AnyValue::Null)
418            .or_else(Expr::lit(f32::NAN))
419            .or_else(Expr::lit(7.0f32));
420        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 7.0);
421    }
422
423    // ---- min_with / max_with ----
424
425    #[test]
426    fn min_with_picks_smaller() {
427        let mut e = Expr::lit(5.0f32).min_with(Expr::lit(3.0f32));
428        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 3.0);
429    }
430
431    #[test]
432    fn max_with_picks_larger() {
433        let mut e = Expr::lit(5.0f32).max_with(Expr::lit(8.0f32));
434        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 8.0);
435    }
436
437    #[test]
438    fn min_with_nan_on_one_side_returns_other() {
439        // f32::min(a, NaN) = a (IEEE 754-2019 minNum semantics)
440        let mut e = Expr::lit(5.0f32).min_with(Expr::lit(f32::NAN));
441        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 5.0);
442    }
443
444    #[test]
445    fn max_with_nan_on_one_side_returns_other() {
446        let mut e = Expr::lit(5.0f32).max_with(Expr::lit(f32::NAN));
447        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 5.0);
448    }
449
450    #[test]
451    fn floor_via_max_with_constant() {
452        // Common pattern: max_with as a floor without an upper ceiling.
453        let mut e = Expr::lit(-3.0f32).max_with(Expr::lit(0.0f32));
454        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
455    }
456
457    // ---- Expr::reset ----
458
459    #[test]
460    fn reset_clears_schedule_counter() {
461        // every(3) fires true on every third call. After two calls + reset,
462        // the next call should NOT fire (counter starts fresh).
463        let mut e = Expr::every(3)
464            .then(Expr::lit(1.0f32))
465            .otherwise(Expr::lit(0.0f32));
466
467        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
468        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
469
470        e.reset();
471
472        // Two more calls — should still be the "otherwise" branch since the
473        // counter restarted at 0.
474        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
475        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
476        // Third call from a fresh counter — should fire.
477        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 1.0);
478    }
479
480    #[test]
481    fn reset_idempotent_on_leaf() {
482        let mut e = Expr::lit(42.0f32);
483        e.reset();
484        e.reset();
485        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 42.0);
486    }
487
488    // ---- Schedule: every(n) ----
489
490    #[test]
491    fn every_fires_on_nth_call_then_resets() {
492        let mut e = Expr::every(3)
493            .then(Expr::Literal(AnyValue::Bool(true)))
494            .otherwise(Expr::Literal(AnyValue::Bool(false)));
495
496        assert!(!bool_val(e.eval(&metrics()).unwrap())); // tick 1
497        assert!(!bool_val(e.eval(&metrics()).unwrap())); // tick 2
498        assert!(bool_val(e.eval(&metrics()).unwrap())); // tick 3 — fires
499        assert!(!bool_val(e.eval(&metrics()).unwrap())); // tick 1 again
500        assert!(!bool_val(e.eval(&metrics()).unwrap())); // tick 2 again
501        assert!(bool_val(e.eval(&metrics()).unwrap())); // tick 3 — fires again
502    }
503
504    // ---- Pre-built composers ----
505
506    fn metrics_with(name: &str, value: f32) -> MetricSet {
507        let mut ms = MetricSet::new();
508        ms.upsert(name, value);
509        ms
510    }
511
512    #[test]
513    fn error_from_method_collapses_to_affine() {
514        // (x - 10) / 10 == x * 0.1 - 1
515        let e = Expr::lit(15.0f32).error(10.0);
516        assert!(is_fused_affine(&e), "expected fused Affine, got {e:?}");
517        let mut e = e;
518        assert!((f32_val(e.eval(&metrics()).unwrap()) - 0.5).abs() < 1e-6);
519    }
520
521    #[test]
522    fn error_from_function_reads_metric() {
523        let ms = metrics_with("foo", 12.0);
524        let mut e = Expr::select("foo").error(10.0);
525        // (12 - 10) / 10 = 0.2
526        assert!((f32_val(e.eval(&ms).unwrap()) - 0.2).abs() < 1e-6);
527    }
528
529    // ---- Streaming quantile (P²) ----
530
531    #[test]
532    fn quantile_stream_returns_first_sample_until_buffer_fills() {
533        let mut e = Expr::select("foo").quantile(0.5);
534        let ms = metrics_with("foo", 5.0);
535        // First sample seeds the estimator; with one sample p50 == that sample.
536        assert!((f32_val(e.eval(&ms).unwrap()) - 5.0).abs() < 1e-6);
537    }
538
539    #[test]
540    fn quantile_stream_null_when_metric_missing() {
541        let mut e = Expr::select("missing").quantile(0.95);
542        let ms = MetricSet::new();
543        assert!(matches!(e.eval(&ms).unwrap(), AnyValue::Null));
544    }
545
546    #[test]
547    fn quantile_stream_converges_on_uniform_sequence() {
548        let mut e = Expr::select("foo").quantile(0.5);
549        let mut ms = MetricSet::new();
550        for i in 1..=200 {
551            ms.upsert("foo", i as f32);
552            let _ = e.eval(&ms);
553        }
554        // True median is 100.5; P² is approximate but should be close.
555        let v = f32_val(e.eval(&ms).unwrap());
556        assert!(
557            (v - 100.5).abs() < 3.0,
558            "p50 estimate {v} far from true median 100.5"
559        );
560    }
561
562    #[test]
563    fn quantile_stream_p95_approximates_high_tail() {
564        let mut e = Expr::select("foo").quantile(0.95);
565        let mut ms = MetricSet::new();
566        for i in 1..=1000 {
567            ms.upsert("foo", i as f32);
568            let _ = e.eval(&ms);
569        }
570        let v = f32_val(e.eval(&ms).unwrap());
571        assert!((v - 950.0).abs() < 20.0, "p95 estimate {v} far from 950");
572    }
573
574    #[test]
575    fn quantile_stream_reset_clears_estimator() {
576        let mut e = Expr::select("foo").quantile(0.5);
577        let mut ms = MetricSet::new();
578        for i in 1..=50 {
579            ms.upsert("foo", i as f32);
580            let _ = e.eval(&ms);
581        }
582        e.reset();
583        // After reset, first eval should produce just-seeded estimator value.
584        ms.upsert("foo", 7.0);
585        let v = f32_val(e.eval(&ms).unwrap());
586        assert!((v - 7.0).abs() < 1e-6, "got {v}");
587    }
588
589    #[test]
590    fn quantile_stream_composes_with_arbitrary_child() {
591        // Stream p50 of a *literal* — exercises the "any child" composition.
592        let mut e = Expr::lit(42.0f32).quantile(0.5);
593        let ms = metrics();
594        let _ = e.eval(&ms);
595        let _ = e.eval(&ms);
596        // After multiple identical samples, p50 == constant.
597        assert!((f32_val(e.eval(&ms).unwrap()) - 42.0).abs() < 1e-6);
598    }
599
600    // ---- Stagnation ----
601
602    #[test]
603    fn stagnation_increments_when_value_unchanged() {
604        let ms = metrics_with("score", 1.0);
605        let mut e = Expr::select("score").stagnation(0.001);
606
607        assert_eq!(f32_val(e.eval(&ms).unwrap()), 0.0); // seed
608        assert_eq!(f32_val(e.eval(&ms).unwrap()), 1.0);
609        assert_eq!(f32_val(e.eval(&ms).unwrap()), 2.0);
610    }
611
612    #[test]
613    fn stagnation_resets_on_large_change() {
614        let mut ms = metrics_with("score", 1.0);
615        let mut e = Expr::select("score").stagnation(0.001);
616
617        let _ = e.eval(&ms);
618        let _ = e.eval(&ms); // count = 1
619
620        ms.upsert("score", 5.0); // big change > epsilon
621        assert_eq!(f32_val(e.eval(&ms).unwrap()), 0.0);
622        assert_eq!(f32_val(e.eval(&ms).unwrap()), 1.0);
623    }
624
625    #[test]
626    fn stagnation_tolerates_tiny_noise() {
627        let mut ms = metrics_with("score", 1.0);
628        let mut e = Expr::select("score").stagnation(0.01);
629
630        let _ = e.eval(&ms);
631        ms.upsert("score", 1.005); // within epsilon
632        assert_eq!(f32_val(e.eval(&ms).unwrap()), 1.0);
633        ms.upsert("score", 1.008);
634        assert_eq!(f32_val(e.eval(&ms).unwrap()), 2.0);
635    }
636
637    #[test]
638    fn stagnation_returns_null_when_metric_missing() {
639        let ms = MetricSet::new();
640        let mut e = Expr::select("missing").stagnation(0.001);
641        assert!(matches!(e.eval(&ms).unwrap(), AnyValue::Null));
642    }
643
644    #[test]
645    fn is_stagnant_fires_at_patience_threshold() {
646        let ms = metrics_with("score", 1.0);
647        let mut e = Expr::select("score").stagnation(0.001).gte(3);
648
649        assert!(!bool_val(e.eval(&ms).unwrap())); // count=0
650        assert!(!bool_val(e.eval(&ms).unwrap())); // count=1
651        assert!(!bool_val(e.eval(&ms).unwrap())); // count=2
652        assert!(bool_val(e.eval(&ms).unwrap())); // count=3, fires
653    }
654
655    #[test]
656    fn stagnation_reset_clears_state() {
657        let ms = metrics_with("score", 1.0);
658        let mut e = Expr::select("score").stagnation(0.001);
659
660        let _ = e.eval(&ms);
661        let _ = e.eval(&ms);
662        let _ = e.eval(&ms); // count = 2
663
664        e.reset();
665        assert_eq!(f32_val(e.eval(&ms).unwrap()), 0.0); // fresh seed
666    }
667
668    #[test]
669    fn is_converged_fires_when_window_is_flat() {
670        // let ms = metrics_with("score", 1.0);
671        // let mut e = Expr::is_converged("score", 3, 0.01);
672
673        // // Buffers seed up to size 3.
674        // let _ = e.eval(&ms);
675        // let _ = e.eval(&ms);
676        // // Third eval: both first and last buffers full, both should hold ~1.0.
677        // assert!(bool_val(e.eval(&ms).unwrap()));
678    }
679
680    #[test]
681    fn is_converged_does_not_fire_when_window_drifts() {
682        // let mut ms = metrics_with("score", 1.0);
683        // let mut e = Expr::is_converged("score", 3, 0.01);
684
685        // let _ = e.eval(&ms);
686        // ms.upsert("score", 2.0);
687        // let _ = e.eval(&ms);
688        // ms.upsert("score", 3.0);
689        // // first=1.0, last=3.0, diff=2.0 > epsilon
690        // assert!(!bool_val(e.eval(&ms).unwrap()));
691    }
692
693    // ---- compile() ----
694
695    #[test]
696    fn compile_folds_pure_literal_subtree() {
697        let e = Expr::lit(2.0f32).add(Expr::lit(3.0f32)).compile();
698        assert!(matches!(e, Expr::Literal(_)));
699        let mut e = e;
700        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 5.0);
701    }
702
703    #[test]
704    fn compile_wraps_metric_plus_lit_as_affine() {
705        let e = Expr::select("foo").add(Expr::lit(3.0f32)).compile();
706        assert!(is_fused_affine(&e), "expected Affine, got {e:?}");
707    }
708
709    #[test]
710    fn compile_collapses_controller_chain_to_single_affine() {
711        // Replicates the species-threshold count_error chain:
712        //   (x - target) / target * GAIN + 1.0
713        //   = x * (GAIN / target) + (1 - GAIN)
714        let target = 10.0f32;
715        let gain = 0.999f32;
716        let e = Expr::select("count.species")
717            .sub(Expr::lit(target))
718            .div(Expr::lit(target))
719            .mul(Expr::lit(gain))
720            .add(Expr::lit(1.0f32))
721            .compile();
722
723        assert!(is_fused_affine(&e), "expected single Affine, got {e:?}");
724    }
725
726    #[test]
727    fn compile_is_idempotent() {
728        let e = Expr::select("foo")
729            .sub(Expr::lit(1.0f32))
730            .mul(Expr::lit(2.0f32))
731            .add(Expr::lit(3.0f32));
732        let once = e.clone().compile();
733        let twice = once.clone().compile();
734        assert_eq!(format!("{:?}", once), format!("{:?}", twice));
735    }
736
737    // ---- Affine ----
738
739    // ---- Composition ----
740
741    #[test]
742    fn composed_expr_add_then_compare() {
743        // (2 + 3) > 4 → true
744        let mut e = Expr::lit(2.0f32)
745            .add(Expr::lit(3.0f32))
746            .gt(Expr::lit(4.0f32));
747        assert!(bool_val(e.eval(&metrics()).unwrap()));
748    }
749
750    #[test]
751    fn composed_expr_clamp_then_scale() {
752        // clamp(-5, 0, 1) * 10 → 0.0
753        let mut e = Expr::lit(-5.0f32)
754            .clamp(Expr::lit(0.0f32), Expr::lit(1.0f32))
755            .mul(Expr::lit(10.0f32));
756        assert_eq!(f32_val(e.eval(&metrics()).unwrap()), 0.0);
757    }
758
759    #[test]
760    fn test_identity_select() {
761        let mut e = Expr::identity().rolling(3).sum();
762
763        for i in 0..5 {
764            let output = e.eval(&i);
765
766            if i < 2 {
767                assert_eq!(f32_val(output.unwrap()), (0..=i).sum::<i32>() as f32);
768            } else {
769                assert_eq!(f32_val(output.unwrap()), (i - 2..=i).sum::<i32>() as f32);
770            }
771        }
772    }
773}