Skip to main content

dotzuki_rules/
trace.rs

1//! The two-layer-debugging trace (doc 11 §5, last bullet): "the interpreter
2//! **must** log `(EffectId, Event, op, relay before/after)` from day one or the
3//! data author is pushed back into Rust." A wrong outcome may be in the data OR
4//! the primitive; this trace tells them apart.
5//!
6//! The trace is **opt-in** (a [`TraceSink`] installed thread-locally by a debug
7//! flag) and records NO entropy — it never draws RNG, never reads a clock, never
8//! affects draw order. It is a pure observer.
9
10#[cfg(not(target_os = "none"))]
11use core::cell::RefCell;
12
13use dotzuki_engine::battle::stack::{EffectId, Event, RelayVar};
14
15use crate::model::Op;
16
17/// One recorded interpreter step (doc 11 §5).
18#[derive(Debug, Clone, PartialEq)]
19pub struct TraceEvent {
20    /// The firing hook's synthesized id (the `source_effect`).
21    pub effect: EffectId,
22    /// The closed event being folded.
23    pub event: Event,
24    /// The op that ran (cloned for inspection).
25    pub op: Op,
26    /// The relay BEFORE the op.
27    pub before: RelayVar,
28    /// The relay AFTER the op (the op's `HandlerResult` applied locally).
29    pub after: RelayVar,
30}
31
32/// A collector of [`TraceEvent`]s. Installed thread-locally by a debug flag so
33/// the interpreter's hot path stays branch-light when tracing is off.
34#[derive(Debug, Default, Clone)]
35pub struct TraceSink {
36    /// The recorded steps, in fold order.
37    pub events: Vec<TraceEvent>,
38}
39
40// Hosted: a thread-local sink (the interpreter may run on a worker thread).
41#[cfg(not(target_os = "none"))]
42thread_local! {
43    static SINK: RefCell<Option<TraceSink>> = const { RefCell::new(None) };
44}
45
46// Bare-metal (GBA): single-threaded by construction, so a plain static is
47// equivalent to the thread-local — there is exactly one "thread" and the
48// interpreter is non-reentrant. Safe for the same reason thread_local is.
49#[cfg(target_os = "none")]
50static mut SINK: Option<TraceSink> = None;
51
52/// Enable tracing for the current thread (the debug flag, doc 11 §5).
53#[cfg_attr(target_os = "none", allow(unsafe_code, static_mut_refs))]
54pub fn enable_trace() {
55    #[cfg(not(target_os = "none"))]
56    SINK.with(|s| *s.borrow_mut() = Some(TraceSink::default()));
57    #[cfg(target_os = "none")]
58    unsafe {
59        SINK = Some(TraceSink::default())
60    }
61}
62
63/// Disable tracing and take the recorded sink (`None` if tracing was off).
64#[cfg_attr(target_os = "none", allow(unsafe_code, static_mut_refs))]
65pub fn take_trace() -> Option<TraceSink> {
66    #[cfg(not(target_os = "none"))]
67    {
68        SINK.with(|s| s.borrow_mut().take())
69    }
70    #[cfg(target_os = "none")]
71    {
72        unsafe { SINK.take() }
73    }
74}
75
76/// Record one step IFF tracing is enabled. Pure: no RNG, no clock, no draw-order
77/// effect.
78#[cfg_attr(target_os = "none", allow(unsafe_code, static_mut_refs))]
79pub(crate) fn record(effect: EffectId, event: Event, op: &Op, before: RelayVar, after: RelayVar) {
80    #[cfg(not(target_os = "none"))]
81    SINK.with(|s| {
82        if let Some(sink) = s.borrow_mut().as_mut() {
83            sink.events.push(TraceEvent {
84                effect,
85                event,
86                op: op.clone(),
87                before,
88                after,
89            });
90        }
91    });
92    #[cfg(target_os = "none")]
93    unsafe {
94        if let Some(sink) = SINK.as_mut() {
95            sink.events.push(TraceEvent {
96                effect,
97                event,
98                op: op.clone(),
99                before,
100                after,
101            });
102        }
103    }
104}