Skip to main content

fluxbench_logic/
context.rs

1//! Metric Context
2//!
3//! Provides variable bindings for expression evaluation.
4
5use evalexpr::{
6    ContextWithMutableVariables, EvalexprError, HashMapContext, Value, eval_with_context,
7};
8use fxhash::FxHashMap;
9use thiserror::Error;
10
11/// Errors from metric evaluation
12#[derive(Debug, Error)]
13#[non_exhaustive]
14pub enum ContextError {
15    #[error("Unknown metric: {0}")]
16    UnknownMetric(String),
17
18    #[error("Evaluation error: {0}")]
19    EvalError(String),
20}
21
22/// Context holding benchmark metrics for expression evaluation
23#[derive(Debug, Clone)]
24pub struct MetricContext {
25    metrics: FxHashMap<String, f64>,
26}
27
28impl MetricContext {
29    /// Create a new empty context
30    pub fn new() -> Self {
31        Self {
32            metrics: FxHashMap::default(),
33        }
34    }
35
36    /// Add a metric value
37    pub fn set(&mut self, name: impl Into<String>, value: f64) {
38        self.metrics.insert(name.into(), value);
39    }
40
41    /// Get a metric value
42    pub fn get(&self, name: &str) -> Option<f64> {
43        self.metrics.get(name).copied()
44    }
45
46    /// Evaluate an expression in this context
47    pub fn evaluate(&self, expression: &str) -> Result<f64, ContextError> {
48        let mut ctx = HashMapContext::new();
49
50        // Add all metrics to evalexpr context
51        // Sanitize `@` to `__` since evalexpr only supports identifier chars
52        for (name, value) in &self.metrics {
53            let safe_name = name.replace('@', "__");
54            ctx.set_value(safe_name, Value::Float(*value))
55                .map_err(|e: EvalexprError| ContextError::EvalError(e.to_string()))?;
56        }
57
58        // Sanitize expression: replace `@` in identifiers with `__`
59        let expression = expression.replace('@', "__");
60
61        // Evaluate
62        let result = eval_with_context(&expression, &ctx)
63            .map_err(|e| ContextError::EvalError(e.to_string()))?;
64
65        // Convert to f64
66        match result {
67            Value::Float(f) => Ok(f),
68            Value::Int(i) => Ok(i as f64),
69            Value::Boolean(b) => Ok(if b { 1.0 } else { 0.0 }),
70            other => Err(ContextError::EvalError(format!(
71                "Expected numeric result, got {:?}",
72                other
73            ))),
74        }
75    }
76
77    /// Check if a metric exists
78    pub fn has(&self, name: &str) -> bool {
79        self.metrics.contains_key(name)
80    }
81
82    /// List all metric names
83    pub fn metric_names(&self) -> impl Iterator<Item = &String> {
84        self.metrics.keys()
85    }
86}
87
88impl Default for MetricContext {
89    fn default() -> Self {
90        Self::new()
91    }
92}
93
94#[cfg(test)]
95mod tests {
96    use super::*;
97
98    #[test]
99    fn test_basic_evaluation() {
100        let mut ctx = MetricContext::new();
101        ctx.set("x", 10.0);
102        ctx.set("y", 5.0);
103
104        let result = ctx.evaluate("x + y").unwrap();
105        assert!((result - 15.0).abs() < f64::EPSILON);
106    }
107
108    #[test]
109    fn test_comparison() {
110        let mut ctx = MetricContext::new();
111        ctx.set("latency", 100.0);
112        ctx.set("threshold", 200.0);
113
114        let result = ctx.evaluate("latency < threshold").unwrap();
115        assert!((result - 1.0).abs() < f64::EPSILON); // true = 1.0
116    }
117
118    #[test]
119    fn test_complex_expression() {
120        let mut ctx = MetricContext::new();
121        ctx.set("raw", 150.0);
122        ctx.set("overhead", 50.0);
123
124        let result = ctx.evaluate("(raw - overhead) < 200").unwrap();
125        assert!((result - 1.0).abs() < f64::EPSILON);
126    }
127}