Skip to main content

rustdv_sim/
phase.rs

1//! Phase triggers (ReadOnly / ReadWrite / NextTimeStep) and the scheduled
2//! write buffer (design-doc §4.1(4), §4.4, mapping rows 15 & 22).
3//!
4//! One hub per sim thread coordinates: writes via `set()` are buffered and
5//! drained at the start of the next ReadWrite phase — before ReadWrite
6//! awaiters are woken — porting cocotb's write scheduler verbatim
7//! (cocotb: handle.py `_apply_scheduled_writes`, `ReadWrite._do_callbacks`).
8//! Illegal phase transitions (write/await-ReadWrite from ReadOnly) are
9//! runtime errors, exactly as cocotb.
10
11use std::cell::{Cell, RefCell};
12use std::future::Future;
13use std::pin::Pin;
14use std::rc::Rc;
15use std::task::{Context, Poll, Waker};
16
17use rustdv_gpi as gpi;
18use rustdv_gpi::LogicArray;
19
20use crate::executor;
21
22#[derive(Copy, Clone, Debug, PartialEq, Eq)]
23pub enum SimPhase {
24    Normal,
25    ReadWrite,
26    ReadOnly,
27}
28
29enum WriteVal {
30    U64(u64),
31    Arr(LogicArray),
32}
33
34struct Hub {
35    phase: Cell<SimPhase>,
36    rw_waiters: RefCell<Vec<Waker>>,
37    rw_cb: RefCell<Option<gpi::CallbackHandle>>,
38    ro_waiters: RefCell<Vec<Waker>>,
39    ro_cb: RefCell<Option<gpi::CallbackHandle>>,
40    nt_waiters: RefCell<Vec<Waker>>,
41    nt_cb: RefCell<Option<gpi::CallbackHandle>>,
42    writes: RefCell<Vec<(gpi::LogicHandle, WriteVal)>>,
43}
44
45thread_local! {
46    static HUB: RefCell<Option<Rc<Hub>>> = const { RefCell::new(None) };
47}
48
49pub(crate) fn init_hub() {
50    HUB.with(|h| {
51        *h.borrow_mut() = Some(Rc::new(Hub {
52            phase: Cell::new(SimPhase::Normal),
53            rw_waiters: RefCell::new(Vec::new()),
54            rw_cb: RefCell::new(None),
55            ro_waiters: RefCell::new(Vec::new()),
56            ro_cb: RefCell::new(None),
57            nt_waiters: RefCell::new(Vec::new()),
58            nt_cb: RefCell::new(None),
59            writes: RefCell::new(Vec::new()),
60        }));
61    });
62}
63
64fn hub() -> Rc<Hub> {
65    HUB.with(|h| h.borrow().clone().expect("rustdv sim context not initialized"))
66}
67
68pub fn current_phase() -> SimPhase {
69    hub().phase.get()
70}
71
72// ---------------------------------------------------------------------------
73// Leaving ReadOnly (D108)
74// ---------------------------------------------------------------------------
75
76/// Return to a region where writing is legal.
77///
78/// The ReadOnly region belongs to the simulator, not to the test that asked
79/// for it. `prime_ro`'s callback sets the phase, wakes the waiters, drains the
80/// executor and only *then* restores `Normal` — and the drain is not confined
81/// to the waiters it woke. Whatever else the executor has queued runs in the
82/// same drain, inside the same `cbReadOnlySynch` callback. When the test that
83/// awaited ReadOnly finishes there, the thing that runs next is the regression
84/// loop, and after that the next test's first statement (D108).
85///
86/// One precision step is the whole of it. There is no cheaper exit: a
87/// zero-delay `cbAfterDelay` registered from inside the ReadOnly callback
88/// would land in the current time step, and Icarus refuses that outright —
89/// `SCHEDULER ERROR: read-only sync events created RW events!` and the run
90/// stops. Once the read-only region of a time step has begun, that time step
91/// has no writable region left, so leaving costs time by construction.
92///
93/// A no-op when the simulation is not in ReadOnly, which is the usual case: a
94/// test whose predecessor ended normally starts exactly where it used to.
95pub async fn leave_read_only() {
96    if hub().phase.get() != SimPhase::ReadOnly {
97        return;
98    }
99    crate::triggers::Timer::steps(1).await;
100}
101
102// ---------------------------------------------------------------------------
103// Write scheduling
104// ---------------------------------------------------------------------------
105
106fn apply_write(h: gpi::LogicHandle, v: &WriteVal) {
107    match v {
108        WriteVal::U64(x) => h.set_u64_now(*x),
109        WriteVal::Arr(a) => h.set_now(a),
110    }
111}
112
113/// The ReadOnly rule, in one place because both write paths owe it: the
114/// scheduled one below and the immediate one in `handle.rs` (D108).
115pub(crate) fn deny_write_in_read_only(h: gpi::LogicHandle) {
116    if hub().phase.get() == SimPhase::ReadOnly {
117        panic!(
118            "illegal write to '{}' during the ReadOnly phase (cocotb rule, design-doc §4.4)",
119            h.full_name()
120        );
121    }
122}
123
124fn schedule(h: gpi::LogicHandle, v: WriteVal) {
125    let hub = hub();
126    match hub.phase.get() {
127        SimPhase::ReadOnly => deny_write_in_read_only(h),
128        SimPhase::ReadWrite => apply_write(h, &v),
129        SimPhase::Normal => {
130            {
131                let mut writes = hub.writes.borrow_mut();
132                if let Some(slot) = writes.iter_mut().find(|(eh, _)| *eh == h) {
133                    slot.1 = v; // last write wins, order preserved
134                } else {
135                    writes.push((h, v));
136                }
137            }
138            prime_rw(&hub);
139        }
140    }
141}
142
143pub(crate) fn schedule_write_u64(h: gpi::LogicHandle, v: u64) {
144    schedule(h, WriteVal::U64(v));
145}
146
147pub(crate) fn schedule_write_arr(h: gpi::LogicHandle, v: LogicArray) {
148    schedule(h, WriteVal::Arr(v));
149}
150
151// ---------------------------------------------------------------------------
152// Priming and firing
153// ---------------------------------------------------------------------------
154
155fn prime_rw(hub: &Rc<Hub>) {
156    if hub.rw_cb.borrow().is_some() {
157        return;
158    }
159    let h = hub.clone();
160    let cb = gpi::register_read_write(Box::new(move || {
161        h.rw_cb.borrow_mut().take(); // fired: safe to drop (released)
162        h.phase.set(SimPhase::ReadWrite);
163        // Drain the write buffer FIRST (cocotb ReadWrite._do_callbacks).
164        let writes: Vec<_> = h.writes.borrow_mut().drain(..).collect();
165        for (sig, val) in &writes {
166            apply_write(*sig, val);
167        }
168        for w in h.rw_waiters.borrow_mut().drain(..) {
169            w.wake();
170        }
171        executor::current().run_until_idle();
172        h.phase.set(SimPhase::Normal);
173    }));
174    *hub.rw_cb.borrow_mut() = Some(cb);
175}
176
177fn prime_ro(hub: &Rc<Hub>) {
178    if hub.ro_cb.borrow().is_some() {
179        return;
180    }
181    let h = hub.clone();
182    let cb = gpi::register_read_only(Box::new(move || {
183        h.ro_cb.borrow_mut().take();
184        h.phase.set(SimPhase::ReadOnly);
185        for w in h.ro_waiters.borrow_mut().drain(..) {
186            w.wake();
187        }
188        executor::current().run_until_idle();
189        h.phase.set(SimPhase::Normal);
190    }));
191    *hub.ro_cb.borrow_mut() = Some(cb);
192}
193
194fn prime_nt(hub: &Rc<Hub>) {
195    if hub.nt_cb.borrow().is_some() {
196        return;
197    }
198    let h = hub.clone();
199    let cb = gpi::register_next_sim_time(Box::new(move || {
200        h.nt_cb.borrow_mut().take();
201        for w in h.nt_waiters.borrow_mut().drain(..) {
202            w.wake();
203        }
204        executor::current().run_until_idle();
205    }));
206    *hub.nt_cb.borrow_mut() = Some(cb);
207}
208
209// ---------------------------------------------------------------------------
210// Awaitables (singleton-trigger analogs, mapping row 15)
211// ---------------------------------------------------------------------------
212
213#[derive(Copy, Clone, PartialEq, Eq)]
214enum PhaseKind {
215    ReadWrite,
216    ReadOnly,
217    NextTimeStep,
218}
219
220pub struct PhaseFut {
221    kind: PhaseKind,
222    registered: bool,
223}
224
225impl Future for PhaseFut {
226    type Output = ();
227    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
228        if self.registered {
229            return Poll::Ready(());
230        }
231        let hub = hub();
232        match self.kind {
233            PhaseKind::ReadWrite => {
234                if hub.phase.get() == SimPhase::ReadOnly {
235                    panic!("awaiting ReadWrite from the ReadOnly phase is illegal (cocotb rule)");
236                }
237                hub.rw_waiters.borrow_mut().push(cx.waker().clone());
238                prime_rw(&hub);
239            }
240            PhaseKind::ReadOnly => {
241                if hub.phase.get() == SimPhase::ReadOnly {
242                    panic!("awaiting ReadOnly from the ReadOnly phase is illegal (cocotb rule)");
243                }
244                hub.ro_waiters.borrow_mut().push(cx.waker().clone());
245                prime_ro(&hub);
246            }
247            PhaseKind::NextTimeStep => {
248                hub.nt_waiters.borrow_mut().push(cx.waker().clone());
249                prime_nt(&hub);
250            }
251        }
252        self.registered = true;
253        Poll::Pending
254    }
255}
256
257/// Await the next ReadWrite phase (port of `ReadWrite()`).
258pub fn read_write() -> PhaseFut {
259    PhaseFut { kind: PhaseKind::ReadWrite, registered: false }
260}
261
262/// Await the next ReadOnly phase (port of `ReadOnly()`).
263pub fn read_only() -> PhaseFut {
264    PhaseFut { kind: PhaseKind::ReadOnly, registered: false }
265}
266
267/// Await the next simulator time step (port of `NextTimeStep()`).
268pub fn next_time_step() -> PhaseFut {
269    PhaseFut { kind: PhaseKind::NextTimeStep, registered: false }
270}