Skip to main content

sv_sim_sim/sched/
repeat.rs

1//! SCHED::REPEAT — `EventTerm::Repeat` engine semantics: tracks how many
2//! times a `repeat(N)@(...)`-shaped dynamic wait's *inner* term must fire
3//! before the waiting process actually resumes (scheduler.md §4; ARCHITECTURE
4//! §6.5; ledger SC-004, SC-005, SC-052, SC-053).
5//!
6//! `SensitivityTable::install_term`'s `Repeat` arm (wave A) installs the
7//! inner term at the *outer* term's index, so every firing of the inner edge
8//! reaches `EngineNotifySink::notify_change` exactly as an ordinary dynamic
9//! match — indistinguishable, at that layer, from a plain `Edge` term firing
10//! once. [`RepeatCounters`] is the missing piece: keyed by `(ProcessId,
11//! term_index)` (never just `ProcessId` —
12//! `two_different_term_indices_for_the_same_process_count_independently`
13//! below proves a process can have more than one live `Repeat` term at once),
14//! it decrements a live per-firing count and reports whether the process
15//! should actually resume yet.
16//!
17//! **SC-053's reduction (documented here, the one place `EventTerm::Repeat`'s
18//! own doc comment in `sched/mod.rs` points back to):** `##N` against a
19//! clocking block's bound clock is semantically `repeat(N)@(posedge
20//! <bound clock's cell>)` — this module counts live *firings* of the inner
21//! term, never ticks or a precomputed absolute time, so "the same compiled
22//! `##N` produces different absolute tick timing when its clock's period
23//! changes dynamically at runtime" falls out for free: there is no cached
24//! tick offset for a period change to invalidate, because none is ever
25//! computed. See `sched::control`'s
26//! `sc053_repeat_based_wait_re_reads_the_clocks_live_period_not_a_cached_tick_offset`
27//! for the scheduler-level (`Engine`-driven) proof of this against a
28//! deliberately irregular (non-periodic) clock.
29
30use rustc_hash::FxHashMap as HashMap;
31use std::sync::Mutex;
32
33use crate::sched::ProcessId;
34
35/// [`RepeatCounters::on_term_fired`]'s report.
36#[derive(Clone, Copy, PartialEq, Eq, Debug)]
37pub enum RepeatOutcome {
38    /// The inner term fired, but fewer than `declared_total` times so far —
39    /// the caller must NOT resume the process: leave its sensitivity entry
40    /// installed and swallow this firing (no enqueue).
41    StillWaiting,
42    /// This firing was the `declared_total`th — the caller may resume the
43    /// process. The counter for this `(proc, term_index)` is removed.
44    Satisfied,
45}
46
47/// Live decrementing counters for every `(ProcessId, term_index)` pair
48/// currently mid-`repeat(N)@(...)` wait. `Mutex`-guarded so `Engine` (which
49/// reaches this only through a shared `&self` inside `EngineNotifySink`) can
50/// call [`Self::on_term_fired`]/[`Self::clear`] from a `&self`-only context —
51/// the same pattern every other `sched` shared-mutable-state field already
52/// uses (`NbaLog`, `RegionQueues`, ...).
53pub struct RepeatCounters {
54    counters: Mutex<HashMap<(ProcessId, usize), u64>>,
55}
56
57impl RepeatCounters {
58    pub fn new() -> Self {
59        Self {
60            counters: Mutex::new(HashMap::default()),
61        }
62    }
63
64    /// Looks up `(proc, term_index)`'s live remaining count, initializing it
65    /// to `declared_total` on first sight (the first firing after the
66    /// process suspended), decrements by 1, and reports the outcome. At `0`
67    /// post-decrement the entry is removed — so a *later*, unrelated
68    /// `repeat(N)@(...)` wait re-installed at the same `term_index` starts
69    /// fresh rather than inheriting a stale zero (see
70    /// `clear_removes_only_the_named_processs_entries` for the companion
71    /// "no stale residue after an explicit clear" proof).
72    pub fn on_term_fired(
73        &self,
74        proc: ProcessId,
75        term_index: usize,
76        declared_total: u64,
77    ) -> RepeatOutcome {
78        let mut counters = self.counters.lock().expect("RepeatCounters mutex poisoned");
79        let remaining = counters.entry((proc, term_index)).or_insert(declared_total);
80        *remaining = remaining.saturating_sub(1);
81        if *remaining == 0 {
82            counters.remove(&(proc, term_index));
83            RepeatOutcome::Satisfied
84        } else {
85            RepeatOutcome::StillWaiting
86        }
87    }
88
89    /// Removes every counter entry belonging to `proc`, regardless of
90    /// `term_index` — called from the same place `SensitivityTable::
91    /// remove_dynamic` already is (`sched::control`'s `dispatch_ready_set`),
92    /// so a process that resumes via any path (an ordinary event match,
93    /// `Disabled`, ...) never leaves a stale partial count behind for a
94    /// future, unrelated dynamic wait reusing the same `term_index`.
95    pub fn clear(&self, proc: ProcessId) {
96        let mut counters = self.counters.lock().expect("RepeatCounters mutex poisoned");
97        counters.retain(|&(p, _), _| p != proc);
98    }
99}
100
101impl Default for RepeatCounters {
102    fn default() -> Self {
103        Self::new()
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    #[test]
112    fn first_and_second_firing_of_a_three_count_repeat_report_still_waiting_third_reports_satisfied()
113     {
114        let counters = RepeatCounters::new();
115        let proc: ProcessId = 1;
116        assert_eq!(
117            counters.on_term_fired(proc, 0, 3),
118            RepeatOutcome::StillWaiting
119        );
120        assert_eq!(
121            counters.on_term_fired(proc, 0, 3),
122            RepeatOutcome::StillWaiting
123        );
124        assert_eq!(counters.on_term_fired(proc, 0, 3), RepeatOutcome::Satisfied);
125    }
126
127    #[test]
128    fn two_different_term_indices_for_the_same_process_count_independently() {
129        let counters = RepeatCounters::new();
130        let proc: ProcessId = 7;
131        // term 0 needs 2, term 1 needs 3, interleaved.
132        assert_eq!(
133            counters.on_term_fired(proc, 0, 2),
134            RepeatOutcome::StillWaiting
135        );
136        assert_eq!(
137            counters.on_term_fired(proc, 1, 3),
138            RepeatOutcome::StillWaiting
139        );
140        assert_eq!(
141            counters.on_term_fired(proc, 1, 3),
142            RepeatOutcome::StillWaiting
143        );
144        assert_eq!(
145            counters.on_term_fired(proc, 0, 2),
146            RepeatOutcome::Satisfied,
147            "term 0 must reach Satisfied at its own declared count (2), unaffected by \
148             term 1's firings"
149        );
150        assert_eq!(
151            counters.on_term_fired(proc, 1, 3),
152            RepeatOutcome::Satisfied,
153            "term 1 must reach Satisfied at its own declared count (3)"
154        );
155    }
156
157    #[test]
158    fn clear_removes_only_the_named_processs_entries() {
159        let counters = RepeatCounters::new();
160        let proc_a: ProcessId = 1;
161        let proc_b: ProcessId = 2;
162        assert_eq!(
163            counters.on_term_fired(proc_a, 0, 3),
164            RepeatOutcome::StillWaiting
165        );
166        assert_eq!(
167            counters.on_term_fired(proc_b, 0, 5),
168            RepeatOutcome::StillWaiting
169        );
170
171        counters.clear(proc_a);
172
173        // proc_a restarts at declared_total - 1 (not continuing a stale
174        // partial count of "1 remaining out of 3").
175        assert_eq!(
176            counters.on_term_fired(proc_a, 0, 3),
177            RepeatOutcome::StillWaiting
178        );
179        assert_eq!(
180            counters.on_term_fired(proc_a, 0, 3),
181            RepeatOutcome::StillWaiting
182        );
183        assert_eq!(
184            counters.on_term_fired(proc_a, 0, 3),
185            RepeatOutcome::Satisfied
186        );
187
188        // proc_b's own entry (1 fired of 5) must be untouched by proc_a's
189        // clear.
190        for _ in 0..3 {
191            assert_eq!(
192                counters.on_term_fired(proc_b, 0, 5),
193                RepeatOutcome::StillWaiting,
194                "proc_b's own in-progress count must survive proc_a's clear untouched"
195            );
196        }
197        assert_eq!(
198            counters.on_term_fired(proc_b, 0, 5),
199            RepeatOutcome::Satisfied
200        );
201    }
202}