wowlab_engine_domain/rotation/
trace.rs1use 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
14pub 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#[derive(Debug)]
88pub 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
98pub fn condition_label_with_values(condition: &Condition, ctx: &mut TraceContext<'_>) -> String {
100 walk_for_label(condition, ctx)
101}
102
103fn 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}