Skip to main content

renew_fixed/
saturation.rs

1//! The saturation counter: how an overflow that is handled stays visible.
2
3use core::cell::Cell;
4
5thread_local! {
6    /// Saturations on this thread since it started.
7    ///
8    /// Thread-local rather than a global atomic, and that is the design
9    /// rather than a concession. Simulation is single-threaded, so
10    /// per-thread is per-simulation: a count attributable to the run that
11    /// produced it, where one global atomic would have merged unrelated
12    /// threads into a number nobody could act on.
13    ///
14    /// It is also the shape this engine's threading rules already permit —
15    /// thread-local storage for diagnostics only, drained through an
16    /// explicit call on the owning thread — so it needs no exception to the
17    /// rule against global mutable state, which a global atomic would have.
18    static SATURATIONS: Cell<u64> = const { Cell::new(0) };
19}
20
21/// How many times arithmetic on this thread saturated.
22///
23/// **Diagnostic only.** Never simulation state, never digested, never
24/// consulted for control flow — a simulation that branched on this would
25/// have made the count part of its own behaviour, and two machines whose
26/// counts differ would then diverge for a reason no digest could explain.
27#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
28pub struct Saturations(pub u64);
29
30impl Saturations {
31    /// Nothing saturated, which is what a test asserts.
32    #[must_use]
33    pub const fn is_clean(self) -> bool {
34        self.0 == 0
35    }
36}
37
38/// Read this thread's saturation count.
39///
40/// The explicit snapshot call `threading.md` requires: the counter is never
41/// read from another thread and never published except through here.
42///
43/// # Example
44///
45/// The shape a test uses — the counter is the alarm, and saturation is what
46/// it is an alarm about:
47///
48/// ```
49/// # use renew_fixed::{Fixed, saturations};
50/// let before = saturations();
51/// let _ = Fixed::MAX + Fixed::ONE;
52/// assert_eq!(saturations().0, before.0 + 1);
53/// ```
54#[must_use]
55pub fn saturations() -> Saturations {
56    Saturations(SATURATIONS.with(Cell::get))
57}
58
59/// Record one saturation. Called only from the arithmetic that saturated.
60pub(crate) fn record() {
61    SATURATIONS.with(|count| count.set(count.get().saturating_add(1)));
62}
63
64#[cfg(test)]
65mod tests {
66    use super::{record, saturations};
67
68    #[test]
69    fn the_counter_counts_and_is_readable_only_through_the_snapshot() {
70        let before = saturations();
71        record();
72        record();
73        assert_eq!(saturations().0, before.0 + 2);
74        assert!(!saturations().is_clean());
75    }
76
77    /// The counter must not itself overflow into wrapping, which would be a
78    /// diagnostic silently claiming fewer failures than occurred.
79    #[test]
80    fn the_counter_saturates_rather_than_wrapping() {
81        super::SATURATIONS.with(|count| count.set(u64::MAX));
82        record();
83        assert_eq!(saturations().0, u64::MAX);
84        super::SATURATIONS.with(|count| count.set(0));
85    }
86
87    /// Per-thread, which is the property that makes it attributable. A
88    /// count raised on another thread must not appear on this one.
89    #[test]
90    // Spawning is disallowed in this crate for good reason: arithmetic has
91    // nothing to parallelise. Proving the counter is *per-thread* is the one
92    // thing that cannot be done without a second thread, so the exemption is
93    // taken here, narrowly, in the test that exists to establish the
94    // property the ban's neighbour depends on.
95    #[expect(
96        clippy::disallowed_methods,
97        reason = "the property under test is thread-locality, which needs a thread"
98    )]
99    fn a_count_on_another_thread_is_not_visible_here() {
100        let before = saturations();
101        std::thread::spawn(|| {
102            record();
103            record();
104            record();
105        })
106        .join()
107        .expect("the counting thread finished");
108        assert_eq!(saturations(), before, "another thread's count leaked here");
109    }
110}