Skip to main content

wowlab_engine_domain/rotation/
trace.rs

1// #t(file: rust_inline_test_module_size) private trace-rendering tests cover the complete condition vocabulary beside the renderer
2
3//! Human-readable labels for rotation condition trees (debug tracing).
4
5use wowlab_types::sim::{Condition, Rotation};
6
7use super::{
8    buffer::{DenseBuffer, DescriptorTable},
9    context::ContextSchema,
10    lower::eval_condition_as,
11    resolver::SpecResolver,
12};
13
14/// Structural label for `cond` (no live values).
15pub fn condition_label(cond: &Condition) -> String {
16    match cond {
17        Condition::Read { field } => {
18            if let Some(key) = &field.key {
19                format!("{}.{}({})", field.domain, field.name, key)
20            } else {
21                format!("{}.{}", field.domain, field.name)
22            }
23        }
24        Condition::Bool { value } => format!("{value}"),
25        Condition::Int { value } => format!("{value}"),
26        Condition::Float { value } => format!("{value}"),
27        Condition::Var { name } => format!("var:{name}"),
28
29        Condition::And { operands } => {
30            let labels: Vec<String> = operands.iter().map(condition_label).collect();
31
32            labels.join(" && ")
33        }
34        Condition::Or { operands } => {
35            let labels: Vec<String> = operands.iter().map(condition_label).collect();
36
37            labels.join(" || ")
38        }
39        Condition::Not { operand } => format!("!{}", condition_label(operand)),
40
41        Condition::Compare { op, left, right } => {
42            format!(
43                "{} {} {}",
44                condition_label(left),
45                op.symbol(),
46                condition_label(right)
47            )
48        }
49
50        Condition::Arith { op, left, right } => {
51            format!(
52                "{} {} {}",
53                condition_label(left),
54                op.symbol(),
55                condition_label(right)
56            )
57        }
58
59        Condition::UnaryMath { op, operand } => {
60            format!("{}({})", op.symbol(), condition_label(operand))
61        }
62
63        Condition::MinMax { op, left, right } => {
64            format!(
65                "{}({},{})",
66                op.symbol(),
67                condition_label(left),
68                condition_label(right)
69            )
70        }
71
72        Condition::IfThenElse {
73            condition,
74            then,
75            otherwise,
76        } => format!(
77            "if({},{},{})",
78            condition_label(condition),
79            condition_label(then),
80            condition_label(otherwise)
81        ),
82        _ => format!("<unsupported condition variant: {cond:?}>"),
83    }
84}
85
86/// Borrowed evaluation context for [`condition_label_with_values`].
87#[derive(Debug)]
88// #t(rust_similar_structs) public trace evaluation adds mutable runtime state to private recorder metadata
89pub struct TraceContext<'a> {
90    pub rotation: &'a Rotation,
91    pub schema: &'a ContextSchema,
92    pub resolver: &'a SpecResolver,
93    pub table: &'a DescriptorTable,
94    pub buffer: &'a mut DenseBuffer,
95    pub now_secs: f64,
96}
97
98/// Label for a false condition, inlining realized buffer values.
99pub fn condition_label_with_values(condition: &Condition, ctx: &mut TraceContext<'_>) -> String {
100    walk_for_label(condition, ctx)
101}
102
103// #t(fn: rust_recursive_fn) only recurses into the first failing `And` operand; depth is bounded by the condition nesting authored in the rotation, not by call volume.
104fn walk_for_label(condition: &Condition, ctx: &mut TraceContext<'_>) -> String {
105    match condition {
106        Condition::And { operands } => {
107            for op in operands {
108                if !eval_bool(op, ctx) {
109                    return walk_for_label(op, ctx);
110                }
111            }
112
113            condition_label(condition)
114        }
115        Condition::Compare { left, right, .. } => {
116            let left_value = eval_float(left, ctx);
117            let right_value = eval_float(right, ctx);
118
119            format!(
120                "{} (was: {} vs {})",
121                condition_label(condition),
122                format_value(left_value),
123                format_value(right_value),
124            )
125        }
126        _ => condition_label(condition),
127    }
128}
129
130fn eval_bool(condition: &Condition, ctx: &mut TraceContext<'_>) -> bool {
131    eval_condition_as(condition, ctx, |value, backend| value.into_bool(backend))
132}
133
134fn eval_float(condition: &Condition, ctx: &mut TraceContext<'_>) -> f64 {
135    eval_condition_as(condition, ctx, |value, backend| value.into_float(backend))
136}
137
138fn format_value(value: f64) -> String {
139    if value.is_nan() {
140        return "NaN".to_string();
141    }
142
143    let rounded = value.round();
144
145    if (value - rounded).abs() < f64::EPSILON {
146        format!(
147            "{}",
148            wowlab_types::numeric::f64_to_i64_saturating_trunc(rounded)
149        )
150    } else {
151        format!("{value:.3}")
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use googletest::prelude::*;
158    use rstest::rstest;
159    use wowlab_types::sim::{ArithOp, CompareOp, FieldRead, MinMaxOp, UnaryMathOp};
160
161    use super::*;
162
163    fn read(domain: &str, name: &str, key: Option<&str>) -> Condition {
164        Condition::Read {
165            field: FieldRead {
166                domain: domain.into(),
167                name: name.into(),
168                key: key.map(Into::into),
169                on: None,
170            },
171        }
172    }
173
174    fn boxed(cond: Condition) -> Box<Condition> {
175        Box::new(cond)
176    }
177
178    #[gtest]
179    fn condition_label_read_without_key() -> Result<()> {
180        verify_that!(
181            condition_label(&read("domain", "name", None)),
182            eq("domain.name")
183        )?;
184
185        Ok(())
186    }
187
188    #[gtest]
189    fn condition_label_read_with_key() -> Result<()> {
190        verify_that!(
191            condition_label(&read("domain", "name", Some("kc"))),
192            eq("domain.name(kc)")
193        )?;
194
195        Ok(())
196    }
197
198    #[gtest]
199    fn condition_label_bool() -> Result<()> {
200        verify_that!(
201            condition_label(&Condition::Bool { value: true }),
202            eq("true")
203        )?;
204
205        Ok(())
206    }
207
208    #[gtest]
209    fn condition_label_int() -> Result<()> {
210        verify_that!(condition_label(&Condition::Int { value: 5 }), eq("5"))?;
211
212        Ok(())
213    }
214
215    #[gtest]
216    fn condition_label_float() -> Result<()> {
217        verify_that!(condition_label(&Condition::Float { value: 2.5 }), eq("2.5"))?;
218
219        Ok(())
220    }
221
222    #[gtest]
223    fn condition_label_var() -> Result<()> {
224        verify_that!(
225            condition_label(&Condition::Var { name: "x".into() }),
226            eq("var:x")
227        )?;
228
229        Ok(())
230    }
231
232    #[gtest]
233    fn condition_label_and() -> Result<()> {
234        let cond = Condition::And {
235            operands: vec![
236                Condition::Var { name: "a".into() },
237                Condition::Var { name: "b".into() },
238            ],
239        };
240
241        verify_that!(condition_label(&cond), eq("var:a && var:b"))?;
242
243        Ok(())
244    }
245
246    #[gtest]
247    fn condition_label_or() -> Result<()> {
248        let cond = Condition::Or {
249            operands: vec![
250                Condition::Var { name: "a".into() },
251                Condition::Var { name: "b".into() },
252            ],
253        };
254
255        verify_that!(condition_label(&cond), eq("var:a || var:b"))?;
256
257        Ok(())
258    }
259
260    #[gtest]
261    fn condition_label_not() -> Result<()> {
262        let cond = Condition::Not {
263            operand: boxed(Condition::Var { name: "a".into() }),
264        };
265
266        verify_that!(condition_label(&cond), eq("!var:a"))?;
267
268        Ok(())
269    }
270
271    #[gtest]
272    fn condition_label_compare() -> Result<()> {
273        let cond = Condition::Compare {
274            op: CompareOp::Gt,
275            left: boxed(Condition::Var { name: "l".into() }),
276            right: boxed(Condition::Var { name: "r".into() }),
277        };
278
279        verify_that!(condition_label(&cond), eq("var:l > var:r"))?;
280
281        Ok(())
282    }
283
284    #[gtest]
285    fn condition_label_arith() -> Result<()> {
286        let cond = Condition::Arith {
287            op: ArithOp::Add,
288            left: boxed(Condition::Var { name: "l".into() }),
289            right: boxed(Condition::Var { name: "r".into() }),
290        };
291
292        verify_that!(condition_label(&cond), eq("var:l + var:r"))?;
293
294        Ok(())
295    }
296
297    #[gtest]
298    fn condition_label_unary_math() -> Result<()> {
299        let cond = Condition::UnaryMath {
300            op: UnaryMathOp::Floor,
301            operand: boxed(Condition::Var { name: "x".into() }),
302        };
303
304        verify_that!(condition_label(&cond), eq("floor(var:x)"))?;
305
306        Ok(())
307    }
308
309    #[gtest]
310    fn condition_label_min_max() -> Result<()> {
311        let cond = Condition::MinMax {
312            op: MinMaxOp::Max,
313            left: boxed(Condition::Var { name: "l".into() }),
314            right: boxed(Condition::Var { name: "r".into() }),
315        };
316
317        verify_that!(condition_label(&cond), eq("max(var:l,var:r)"))?;
318
319        Ok(())
320    }
321
322    #[gtest]
323    fn condition_label_if_then_else() -> Result<()> {
324        let cond = Condition::IfThenElse {
325            condition: boxed(Condition::Var { name: "c".into() }),
326            then: boxed(Condition::Var { name: "t".into() }),
327            otherwise: boxed(Condition::Var { name: "e".into() }),
328        };
329
330        verify_that!(condition_label(&cond), eq("if(var:c,var:t,var:e)"))?;
331
332        Ok(())
333    }
334
335    #[gtest]
336    fn condition_label_nested_composite_snapshot() {
337        let cond = Condition::And {
338            operands: vec![
339                Condition::Compare {
340                    op: CompareOp::Gt,
341                    left: boxed(read("cooldown", "charges", Some("kc"))),
342                    right: boxed(Condition::Int { value: 1 }),
343                },
344                Condition::Not {
345                    operand: boxed(Condition::Var {
346                        name: "pooling".into(),
347                    }),
348                },
349            ],
350        };
351
352        insta::assert_snapshot!(condition_label(&cond));
353    }
354
355    #[gtest]
356    #[rstest]
357    #[case::nan(f64::NAN, "NaN")]
358    #[case::positive_integer(3.0, "3")]
359    #[case::negative_integer(-2.0, "-2")]
360    #[case::decimal(2.5, "2.500")]
361    #[case::epsilon_boundary(0.0001, "0.000")]
362    fn format_value_arms(#[case] value: f64, #[case] expected: &str) -> Result<()> {
363        verify_that!(format_value(value), eq(expected))?;
364
365        Ok(())
366    }
367}