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}