Skip to main content

wowlab_engine_domain/rotation/
interp_backend.rs

1// #t(file: rust_floating_point_eq) `float_to_bool` is the documented exact-zero check that mirrors the trait coercion contract.
2// #t(file: rust_bool_params) `and_b`/`or_b`/`select_b` are trait primitives whose Bool params are unavoidable.
3
4//! Interpreter side of the [`RotationBackend`] lowering trait.
5
6use std::marker::PhantomData;
7
8use wowlab_types::{
9    constants::MS_PER_SECOND,
10    numeric::{float_eq, float_ne, safe_div, true_mod},
11    sim::{Condition, Rotation, RotationAction, SpellIdx},
12};
13
14use super::{
15    backend::RotationBackend,
16    buffer::{DenseBuffer, DescriptorTable},
17    context::ContextSchema,
18    decision_trace::{
19        ActionEvaluation, ActionOutcome, ActionRecord, Decision, DecisionTraceSink,
20        DecisionTraceTarget, EvaluationStatus,
21    },
22    resolver::SpecResolver,
23    result::EvalResult,
24    trace::{TraceContext, condition_label_with_values},
25};
26
27#[derive(Clone, Copy)]
28pub(crate) struct RecorderCtx<'a> {
29    pub(crate) rotation: &'a Rotation,
30    pub(crate) schema: &'a ContextSchema,
31    pub(crate) resolver: &'a SpecResolver,
32    pub(crate) table: &'a DescriptorTable,
33}
34
35pub(crate) struct InterpFrame<'a> {
36    pub(crate) buffer: &'a mut DenseBuffer,
37    pub(crate) now: f64,
38}
39
40struct TraceState<'a> {
41    sink: DecisionTraceTarget,
42    ctx: RecorderCtx<'a>,
43    buffer: *mut DenseBuffer,
44    stack: Vec<Decision>,
45    time_ms: u32,
46}
47
48pub(crate) struct InterpBackend<'a> {
49    buf: *mut u8,
50    len: usize,
51    now: f64,
52    result: Option<EvalResult>,
53    trace: Option<TraceState<'a>>,
54    _phantom: PhantomData<&'a mut DenseBuffer>,
55}
56
57impl<'a> InterpBackend<'a> {
58    pub(crate) fn new(frame: InterpFrame<'a>) -> Self {
59        let InterpFrame { buffer: buf, now } = frame;
60        let len = buf.bytes().len();
61
62        Self {
63            buf: buf.as_mut_ptr(),
64            len,
65            now,
66            result: None,
67            trace: None,
68            _phantom: PhantomData,
69        }
70    }
71
72    pub(crate) fn with_sink(
73        frame: InterpFrame<'a>,
74        sink: DecisionTraceTarget,
75        ctx: RecorderCtx<'a>,
76    ) -> Self {
77        let InterpFrame { buffer: buf, now } = frame;
78        let time_ms = wowlab_types::numeric::f64_to_u32_saturating_round(now * MS_PER_SECOND);
79        let len = buf.bytes().len();
80        let buffer_ptr: *mut DenseBuffer = buf;
81        let data_ptr = buf.as_mut_ptr();
82
83        Self {
84            buf: data_ptr,
85            len,
86            now,
87            result: None,
88            trace: Some(TraceState {
89                sink,
90                ctx,
91                buffer: buffer_ptr,
92                stack: Vec::new(),
93                time_ms,
94            }),
95            _phantom: PhantomData,
96        }
97    }
98
99    pub(crate) fn finish(self) -> EvalResult {
100        if let Some(state) = self.trace {
101            for d in state.stack.into_iter().rev() {
102                state.sink.record(d);
103            }
104        }
105
106        self.result.unwrap_or(EvalResult::NONE)
107    }
108
109    #[inline]
110    fn bytes(&self) -> &[u8] {
111        // SAFETY: exclusive `&mut DenseBuffer` borrow via `PhantomData`; buffer not resized, reads never aliased with stores.
112        unsafe { std::slice::from_raw_parts(self.buf, self.len) }
113    }
114
115    #[inline]
116    fn capture(&mut self, r: EvalResult) {
117        if self.result.is_none() {
118            self.result = Some(r);
119        }
120    }
121}
122
123impl TraceState<'_> {
124    fn rejection_reason(&self, now_secs: f64, failing: &Condition) -> Option<String> {
125        // SAFETY: InterpBackend holds the exclusive borrow via PhantomData; recorder runs between lowerer buffer ops, so no concurrent mutation.
126        let buffer = unsafe { &mut *self.buffer };
127        let mut tc = TraceContext {
128            rotation: self.ctx.rotation,
129            schema: self.ctx.schema,
130            resolver: self.ctx.resolver,
131            table: self.ctx.table,
132            buffer,
133            now_secs,
134        };
135        let label = condition_label_with_values(failing, &mut tc);
136
137        if label.is_empty() { None } else { Some(label) }
138    }
139}
140
141impl RotationBackend for InterpBackend<'_> {
142    type Bool = bool;
143    type Int = i64;
144    type Float = f64;
145
146    #[inline]
147    fn const_bool(&mut self, v: bool) -> bool {
148        v
149    }
150    #[inline]
151    fn const_i32(&mut self, v: i32) -> i64 {
152        i64::from(v)
153    }
154    #[inline]
155    fn const_i64(&mut self, v: i64) -> i64 {
156        v
157    }
158    #[inline]
159    fn const_f64(&mut self, v: f64) -> f64 {
160        v
161    }
162
163    #[inline]
164    fn load_bool(&mut self, offset: usize) -> bool {
165        // #t(block: rust_unchecked_indexing) offset is schema-validated to lie within byte_len; bool fields are stored as i32
166        bytemuck::pod_read_unaligned::<i32>(&self.bytes()[offset..offset + size_of::<i32>()]) != 0
167    }
168    #[inline]
169    fn load_i32(&mut self, offset: usize) -> i64 {
170        // #t(block: rust_unchecked_indexing) offset is schema-validated to lie within byte_len
171        i64::from(bytemuck::pod_read_unaligned::<i32>(
172            &self.bytes()[offset..offset + size_of::<i32>()],
173        ))
174    }
175    #[inline]
176    fn load_i64(&mut self, offset: usize) -> i64 {
177        // #t(block: rust_unchecked_indexing) offset is schema-validated to lie within byte_len
178        bytemuck::pod_read_unaligned::<i64>(&self.bytes()[offset..offset + size_of::<i64>()])
179    }
180    #[inline]
181    fn load_f64(&mut self, offset: usize) -> f64 {
182        // #t(block: rust_unchecked_indexing) offset is schema-validated to lie within byte_len
183        bytemuck::pod_read_unaligned::<f64>(&self.bytes()[offset..offset + size_of::<f64>()])
184    }
185
186    #[inline]
187    fn store_bool(&mut self, offset: usize, v: bool) {
188        let bytes = i32::from(v).to_ne_bytes();
189        // SAFETY: offset and width are schema-validated, and the buffer remains exclusively borrowed.
190
191        unsafe { std::slice::from_raw_parts_mut(self.buf.add(offset), bytes.len()) }
192            .copy_from_slice(&bytes);
193    }
194    #[inline]
195    fn store_i32(&mut self, offset: usize, v: i64) {
196        let bytes = crate::numeric::wrapping_i64_to_i32(v).to_ne_bytes();
197        // SAFETY: offset and width are schema-validated, and the buffer remains exclusively borrowed.
198
199        unsafe { std::slice::from_raw_parts_mut(self.buf.add(offset), bytes.len()) }
200            .copy_from_slice(&bytes);
201    }
202    #[inline]
203    fn store_f64(&mut self, offset: usize, v: f64) {
204        let bytes = v.to_ne_bytes();
205        // SAFETY: offset and width are schema-validated, and the buffer remains exclusively borrowed.
206
207        unsafe { std::slice::from_raw_parts_mut(self.buf.add(offset), bytes.len()) }
208            .copy_from_slice(&bytes);
209    }
210
211    #[inline]
212    fn now(&mut self) -> f64 {
213        self.now
214    }
215
216    #[inline]
217    fn add_f(&mut self, a: f64, b: f64) -> f64 {
218        a + b
219    }
220    #[inline]
221    fn sub_f(&mut self, a: f64, b: f64) -> f64 {
222        a - b
223    }
224    #[inline]
225    fn mul_f(&mut self, a: f64, b: f64) -> f64 {
226        a * b
227    }
228    #[inline]
229    fn safe_div_f(&mut self, a: f64, b: f64) -> f64 {
230        safe_div(a, b)
231    }
232    #[inline]
233    fn true_mod_f(&mut self, a: f64, b: f64) -> f64 {
234        true_mod(a, b)
235    }
236    #[inline]
237    fn min_f(&mut self, a: f64, b: f64) -> f64 {
238        a.min(b)
239    }
240    #[inline]
241    fn max_f(&mut self, a: f64, b: f64) -> f64 {
242        a.max(b)
243    }
244    #[inline]
245    fn floor_f(&mut self, v: f64) -> f64 {
246        v.floor()
247    }
248    #[inline]
249    fn ceil_f(&mut self, v: f64) -> f64 {
250        v.ceil()
251    }
252    #[inline]
253    fn abs_f(&mut self, v: f64) -> f64 {
254        v.abs()
255    }
256
257    #[inline]
258    fn cmp_gt_f(&mut self, a: f64, b: f64) -> bool {
259        a > b
260    }
261    #[inline]
262    fn cmp_gte_f(&mut self, a: f64, b: f64) -> bool {
263        a >= b
264    }
265    #[inline]
266    fn cmp_lt_f(&mut self, a: f64, b: f64) -> bool {
267        a < b
268    }
269    #[inline]
270    fn cmp_lte_f(&mut self, a: f64, b: f64) -> bool {
271        a <= b
272    }
273    #[inline]
274    fn cmp_eq_f(&mut self, a: f64, b: f64) -> bool {
275        float_eq(a, b)
276    }
277    #[inline]
278    fn cmp_ne_f(&mut self, a: f64, b: f64) -> bool {
279        float_ne(a, b)
280    }
281    #[inline]
282    fn cmp_eq_i(&mut self, a: i64, b: i64) -> bool {
283        a == b
284    }
285    #[inline]
286    fn cmp_ne_i(&mut self, a: i64, b: i64) -> bool {
287        a != b
288    }
289    #[inline]
290    fn cmp_gt_i(&mut self, a: i64, b: i64) -> bool {
291        a > b
292    }
293    #[inline]
294    fn cmp_gte_i(&mut self, a: i64, b: i64) -> bool {
295        a >= b
296    }
297    #[inline]
298    fn cmp_lt_i(&mut self, a: i64, b: i64) -> bool {
299        a < b
300    }
301    #[inline]
302    fn cmp_lte_i(&mut self, a: i64, b: i64) -> bool {
303        a <= b
304    }
305
306    #[inline]
307    fn and_b(&mut self, a: bool, b: bool) -> bool {
308        a && b
309    }
310    #[inline]
311    fn or_b(&mut self, a: bool, b: bool) -> bool {
312        a || b
313    }
314    #[inline]
315    fn not_b(&mut self, a: bool) -> bool {
316        !a
317    }
318
319    #[inline]
320    fn bool_to_float(&mut self, v: bool) -> f64 {
321        if v { 1.0 } else { 0.0 }
322    }
323    #[inline]
324    fn bool_to_int(&mut self, v: bool) -> i64 {
325        i64::from(v)
326    }
327    #[inline]
328    fn int_to_float(&mut self, v: i64) -> f64 {
329        wowlab_types::numeric::i64_to_f64(v)
330    }
331    #[inline]
332    fn int_to_bool(&mut self, v: i64) -> bool {
333        v != 0
334    }
335    #[inline]
336    fn float_to_bool(&mut self, v: f64) -> bool {
337        v != 0.0
338    }
339    #[inline]
340    fn float_to_int(&mut self, v: f64) -> i64 {
341        wowlab_types::numeric::f64_to_i64_saturating_trunc(v)
342    }
343
344    #[inline]
345    fn select_f(&mut self, cond: bool, t: f64, f: f64) -> f64 {
346        if cond { t } else { f }
347    }
348    #[inline]
349    fn select_i(&mut self, cond: bool, t: i64, f: i64) -> i64 {
350        if cond { t } else { f }
351    }
352    #[inline]
353    fn select_b(&mut self, cond: bool, t: bool, f: bool) -> bool {
354        if cond { t } else { f }
355    }
356
357    #[inline]
358    fn return_none_if(&mut self, cond: bool) {
359        if cond {
360            self.capture(EvalResult::NONE);
361        }
362    }
363    #[inline]
364    fn return_cast_if(&mut self, cond: bool, spell_id: u32, empower_rank: u8) {
365        if cond {
366            self.capture(EvalResult::cast(SpellIdx(spell_id), empower_rank));
367        }
368    }
369    #[inline]
370    fn return_wait_if(&mut self, cond: bool, seconds: f32) {
371        if cond {
372            self.capture(EvalResult::wait(seconds));
373        }
374    }
375    #[inline]
376    fn return_pool_if(&mut self, cond: bool, target: f32) {
377        if cond {
378            self.capture(EvalResult::pool(target));
379        }
380    }
381    #[inline]
382    fn return_use_item_if(&mut self, cond: bool, gear_slot: u8, empower_rank: u8) {
383        if cond {
384            self.capture(EvalResult::use_item_raw(gear_slot, empower_rank));
385        }
386    }
387    #[inline]
388    fn return_none(&mut self) {
389        self.capture(EvalResult::NONE);
390    }
391
392    #[inline]
393    fn record_action(&mut self, record: ActionRecord<'_>) {
394        let now_secs = self.now;
395        let Some(state) = self.trace.as_mut() else {
396            return;
397        };
398        let status = record.outcome.status();
399        let rejection_reason = match &record.outcome {
400            ActionOutcome::Rejected { failing } => state.rejection_reason(now_secs, failing),
401            _ => None,
402        };
403        let Some(decision) = state.stack.last_mut() else {
404            return;
405        };
406
407        decision.evaluations.push(ActionEvaluation {
408            list_id: record.list_id.to_string(),
409            action_index: record.action_index,
410            status,
411            rejection_reason,
412        });
413
414        if matches!(record.outcome, ActionOutcome::Fired) {
415            decision.fired_action_index = Some(record.action_index);
416        }
417    }
418
419    #[inline]
420    fn record_action_guarded(
421        &mut self,
422        list_id: &str,
423        action_index: usize,
424        _action: &RotationAction,
425        is_terminator: bool,
426        guard_held: bool,
427        failing: &Condition,
428    ) {
429        let now_secs = self.now;
430        let Some(state) = self.trace.as_mut() else {
431            return;
432        };
433        let already_fired = state
434            .stack
435            .last()
436            .is_some_and(|d| d.fired_action_index.is_some());
437        let (status, rejection_reason) = if already_fired {
438            (EvaluationStatus::NotReached, None)
439        } else if guard_held {
440            if is_terminator {
441                (EvaluationStatus::Fired, None)
442            } else {
443                (EvaluationStatus::Executed, None)
444            }
445        } else {
446            let reason = state.rejection_reason(now_secs, failing);
447
448            (EvaluationStatus::Rejected, reason)
449        };
450        let Some(decision) = state.stack.last_mut() else {
451            return;
452        };
453
454        decision.evaluations.push(ActionEvaluation {
455            list_id: list_id.to_string(),
456            action_index,
457            status,
458            rejection_reason,
459        });
460
461        if status == EvaluationStatus::Fired {
462            decision.fired_action_index = Some(action_index);
463        }
464    }
465
466    #[inline]
467    fn begin_list_recording(&mut self, list_id: &str) {
468        let Some(state) = self.trace.as_mut() else {
469            return;
470        };
471
472        state.stack.push(Decision {
473            time_ms: state.time_ms,
474            list_id: list_id.to_string(),
475            fired_action_index: None,
476            evaluations: Vec::new(),
477            nested: Vec::new(),
478        });
479    }
480
481    #[inline]
482    fn end_list_recording(&mut self) {
483        let Some(state) = self.trace.as_mut() else {
484            return;
485        };
486        let Some(finished) = state.stack.pop() else {
487            return;
488        };
489
490        if let Some(parent) = state.stack.last_mut() {
491            parent.nested.push(finished);
492        } else {
493            state.sink.record(finished);
494        }
495    }
496}