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, Weak};
15use std::task::{Context, Poll};
16
17use rustdv_gpi as gpi;
18use rustdv_gpi::{BigUint, LogicArray};
19
20use crate::{executor, triggers::TrigShared};
21
22#[derive(Copy, Clone, Debug, PartialEq, Eq)]
23pub enum SimPhase {
24    Normal,
25    ReadWrite,
26    ReadOnly,
27}
28
29pub(crate) enum WriteVal {
30    Bool(bool),
31    U8(u8),
32    U16(u16),
33    U32(u32),
34    U64(u64),
35    U128(u128),
36    BigInt(BigUint),
37    Logic(LogicArray),
38    Real(f64),
39    String(String),
40}
41
42#[derive(Copy, Clone, PartialEq, Eq)]
43pub(crate) enum WriteTarget {
44    Logic(gpi::LogicHandle),
45    Real(gpi::RealHandle),
46    String(gpi::StringHandle),
47}
48impl WriteTarget {
49    fn full_name(self) -> String {
50        match self {
51            Self::Logic(h) => h.full_name(),
52            Self::Real(h) => h.full_name(),
53            Self::String(h) => h.full_name(),
54        }
55    }
56}
57impl From<gpi::LogicHandle> for WriteTarget {
58    fn from(h: gpi::LogicHandle) -> Self {
59        Self::Logic(h)
60    }
61}
62impl From<gpi::RealHandle> for WriteTarget {
63    fn from(h: gpi::RealHandle) -> Self {
64        Self::Real(h)
65    }
66}
67impl From<gpi::StringHandle> for WriteTarget {
68    fn from(h: gpi::StringHandle) -> Self {
69        Self::String(h)
70    }
71}
72
73struct Hub {
74    phase: Cell<SimPhase>,
75    rw_waiters: RefCell<Vec<Weak<TrigShared>>>,
76    rw_cb: RefCell<Option<gpi::CallbackHandle>>,
77    ro_waiters: RefCell<Vec<Weak<TrigShared>>>,
78    ro_cb: RefCell<Option<gpi::CallbackHandle>>,
79    nt_waiters: RefCell<Vec<Weak<TrigShared>>>,
80    nt_cb: RefCell<Option<gpi::CallbackHandle>>,
81    writes: RefCell<Vec<(WriteTarget, WriteVal)>>,
82}
83
84thread_local! {
85    static HUB: RefCell<Option<Rc<Hub>>> = const { RefCell::new(None) };
86}
87
88pub(crate) fn init_hub() {
89    HUB.with(|h| {
90        *h.borrow_mut() = Some(Rc::new(Hub {
91            phase: Cell::new(SimPhase::Normal),
92            rw_waiters: RefCell::new(Vec::new()),
93            rw_cb: RefCell::new(None),
94            ro_waiters: RefCell::new(Vec::new()),
95            ro_cb: RefCell::new(None),
96            nt_waiters: RefCell::new(Vec::new()),
97            nt_cb: RefCell::new(None),
98            writes: RefCell::new(Vec::new()),
99        }));
100    });
101}
102
103fn hub() -> Rc<Hub> {
104    HUB.with(|h| {
105        h.borrow()
106            .clone()
107            .expect("rustdv sim context not initialized")
108    })
109}
110
111pub fn current_phase() -> SimPhase {
112    hub().phase.get()
113}
114
115pub(crate) fn current_phase_if_initialized() -> Option<SimPhase> {
116    HUB.with(|hub| hub.borrow().as_ref().map(|hub| hub.phase.get()))
117}
118
119// ---------------------------------------------------------------------------
120// Leaving ReadOnly (D108)
121// ---------------------------------------------------------------------------
122
123/// Return to a region where writing is legal.
124///
125/// The ReadOnly region belongs to the simulator, not to the test that asked
126/// for it. `prime_ro`'s callback sets the phase, wakes the waiters, drains the
127/// executor and only *then* restores `Normal` — and the drain is not confined
128/// to the waiters it woke. Whatever else the executor has queued runs in the
129/// same drain, inside the same `cbReadOnlySynch` callback. When the test that
130/// awaited ReadOnly finishes there, the thing that runs next is the regression
131/// loop, and after that the next test's first statement (D108).
132///
133/// One precision step is the whole of it. There is no cheaper exit: a
134/// zero-delay `cbAfterDelay` registered from inside the ReadOnly callback
135/// would land in the current time step, and Icarus refuses that outright —
136/// `SCHEDULER ERROR: read-only sync events created RW events!` and the run
137/// stops. Once the read-only region of a time step has begun, that time step
138/// has no writable region left, so leaving costs time by construction.
139///
140/// A no-op when the simulation is not in ReadOnly, which is the usual case: a
141/// test whose predecessor ended normally starts exactly where it used to.
142pub async fn leave_read_only() {
143    if hub().phase.get() != SimPhase::ReadOnly {
144        return;
145    }
146    crate::triggers::Timer::steps(1).await;
147}
148
149// ---------------------------------------------------------------------------
150// Write scheduling
151// ---------------------------------------------------------------------------
152
153fn apply_write(target: WriteTarget, v: &WriteVal) {
154    match (target, v) {
155        (WriteTarget::Real(h), WriteVal::Real(x)) => {
156            h.set_now(*x);
157            return;
158        }
159        (WriteTarget::String(h), WriteVal::String(x)) => {
160            h.set_now(x).expect("scheduled string was validated");
161            return;
162        }
163        _ => {}
164    }
165    let WriteTarget::Logic(h) = target else {
166        unreachable!("write target/value mismatch")
167    };
168    match v {
169        WriteVal::Bool(x) => h.set_bool_now(*x),
170        WriteVal::U8(x) => h.set_u8_now(*x),
171        WriteVal::U16(x) => h.set_u16_now(*x),
172        WriteVal::U32(x) => h.set_u32_now(*x),
173        WriteVal::U64(x) => h.set_u64_now(*x),
174        WriteVal::U128(x) => h.set_u128_now(*x),
175        WriteVal::BigInt(x) => h.set_bigint_now(x),
176        WriteVal::Logic(x) => h.set_logic_now(x),
177        _ => unreachable!("write target/value mismatch"),
178    }
179}
180
181/// The ReadOnly rule, in one place because both write paths owe it: the
182/// scheduled one below and the immediate methods in the `handle` submodules (D108).
183pub(crate) fn deny_write_in_read_only(h: impl Into<WriteTarget>) {
184    let h = h.into();
185    if hub().phase.get() == SimPhase::ReadOnly {
186        panic!(
187            "illegal write to '{}' during the ReadOnly phase (cocotb rule, design-doc §4.4)",
188            h.full_name()
189        );
190    }
191}
192
193pub(crate) fn schedule(h: impl Into<WriteTarget>, v: WriteVal) {
194    let h = h.into();
195    let hub = hub();
196    match hub.phase.get() {
197        SimPhase::ReadOnly => deny_write_in_read_only(h),
198        SimPhase::ReadWrite => apply_write(h, &v),
199        SimPhase::Normal => {
200            {
201                let mut writes = hub.writes.borrow_mut();
202                if let Some(slot) = writes.iter_mut().find(|(eh, _)| *eh == h) {
203                    slot.1 = v; // last write wins, order preserved
204                } else {
205                    writes.push((h, v));
206                }
207            }
208            prime_rw(&hub);
209        }
210    }
211}
212
213// ---------------------------------------------------------------------------
214// Priming and firing
215// ---------------------------------------------------------------------------
216
217// Detach the batch before waking tasks: waits created during the executor
218// drain belong to a subsequent callback. Weak entries do not retain cancelled
219// futures or their task wakers, and cancellation never removes shared work.
220fn fire_waiters(waiters: &RefCell<Vec<Weak<TrigShared>>>) {
221    for waiter in std::mem::take(&mut *waiters.borrow_mut()) {
222        if let Some(shared) = waiter.upgrade() {
223            shared.fire();
224        }
225    }
226}
227
228fn prime_rw(hub: &Rc<Hub>) {
229    if hub.rw_cb.borrow().is_some() {
230        return;
231    }
232    let h = hub.clone();
233    let cb = gpi::register_read_write(Box::new(move || {
234        h.rw_cb.borrow_mut().take(); // fired: safe to drop (released)
235        h.phase.set(SimPhase::ReadWrite);
236        // Drain the write buffer FIRST (cocotb ReadWrite._do_callbacks).
237        let writes: Vec<_> = h.writes.borrow_mut().drain(..).collect();
238        for (sig, val) in &writes {
239            apply_write(*sig, val);
240        }
241        fire_waiters(&h.rw_waiters);
242        executor::current().run_until_idle();
243        h.phase.set(SimPhase::Normal);
244    }));
245    *hub.rw_cb.borrow_mut() = Some(cb);
246}
247
248fn prime_ro(hub: &Rc<Hub>) {
249    if hub.ro_cb.borrow().is_some() {
250        return;
251    }
252    let h = hub.clone();
253    let cb = gpi::register_read_only(Box::new(move || {
254        h.ro_cb.borrow_mut().take();
255        h.phase.set(SimPhase::ReadOnly);
256        fire_waiters(&h.ro_waiters);
257        executor::current().run_until_idle();
258        h.phase.set(SimPhase::Normal);
259    }));
260    *hub.ro_cb.borrow_mut() = Some(cb);
261}
262
263fn prime_nt(hub: &Rc<Hub>) {
264    if hub.nt_cb.borrow().is_some() {
265        return;
266    }
267    let h = hub.clone();
268    let cb = gpi::register_next_sim_time(Box::new(move || {
269        h.nt_cb.borrow_mut().take();
270        fire_waiters(&h.nt_waiters);
271        executor::current().run_until_idle();
272    }));
273    *hub.nt_cb.borrow_mut() = Some(cb);
274}
275
276// ---------------------------------------------------------------------------
277// Awaitables (singleton-trigger analogs, mapping row 15)
278// ---------------------------------------------------------------------------
279
280#[derive(Copy, Clone, PartialEq, Eq)]
281enum PhaseKind {
282    ReadWrite,
283    ReadOnly,
284    NextTimeStep,
285}
286
287pub struct PhaseFut {
288    kind: PhaseKind,
289    shared: Option<Rc<TrigShared>>,
290}
291
292impl Future for PhaseFut {
293    type Output = ();
294    fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
295        if let Some(shared) = &self.shared {
296            if shared.fired() {
297                return Poll::Ready(());
298            }
299            shared.set_waker(cx.waker().clone());
300            return Poll::Pending;
301        }
302        let hub = hub();
303        let shared = TrigShared::new();
304        shared.set_waker(cx.waker().clone());
305        match self.kind {
306            PhaseKind::ReadWrite => {
307                if hub.phase.get() == SimPhase::ReadOnly {
308                    panic!("awaiting ReadWrite from the ReadOnly phase is illegal (cocotb rule)");
309                }
310                hub.rw_waiters.borrow_mut().push(Rc::downgrade(&shared));
311                prime_rw(&hub);
312            }
313            PhaseKind::ReadOnly => {
314                if hub.phase.get() == SimPhase::ReadOnly {
315                    panic!("awaiting ReadOnly from the ReadOnly phase is illegal (cocotb rule)");
316                }
317                hub.ro_waiters.borrow_mut().push(Rc::downgrade(&shared));
318                prime_ro(&hub);
319            }
320            PhaseKind::NextTimeStep => {
321                hub.nt_waiters.borrow_mut().push(Rc::downgrade(&shared));
322                prime_nt(&hub);
323            }
324        }
325        self.shared = Some(shared);
326        Poll::Pending
327    }
328}
329
330/// Await the next ReadWrite phase (port of `ReadWrite()`).
331pub fn read_write() -> PhaseFut {
332    PhaseFut {
333        kind: PhaseKind::ReadWrite,
334        shared: None,
335    }
336}
337
338/// Await the next ReadOnly phase (port of `ReadOnly()`).
339pub fn read_only() -> PhaseFut {
340    PhaseFut {
341        kind: PhaseKind::ReadOnly,
342        shared: None,
343    }
344}
345
346/// Run one synchronous service operation at a settled ReadOnly point.
347///
348/// The closure runs on the simulator thread from inside the simulator's
349/// ReadOnly callback, after pending writes and combinational logic have
350/// reached a fixed point. It may communicate with other OS threads through
351/// ordinary synchronization primitives. While it is waiting, the simulator
352/// callback cannot return, so simulation time remains frozen. Returning the
353/// closure's value releases the simulator to advance normally.
354///
355/// # Blocking and cancellation
356///
357/// Any wait in `service` must be bounded by wall-clock time or use a
358/// cooperative cancellation signal. A simulation-time timeout cannot fire
359/// while this function holds ReadOnly, and RustDV cannot forcibly recover an
360/// arbitrary blocking closure. An unbounded wait will therefore wedge the
361/// simulation.
362///
363/// This is opt-in and transport-neutral: RustDV does not create a worker,
364/// scheduler, or server. A testbench supplies both the service closure and any
365/// channels or synchronization it needs.
366pub async fn service_read_only<F, R>(service: F) -> R
367where
368    F: FnOnce() -> R,
369{
370    read_only().await;
371    service()
372}
373
374/// Await the next simulator time step (port of `NextTimeStep()`).
375pub fn next_time_step() -> PhaseFut {
376    PhaseFut {
377        kind: PhaseKind::NextTimeStep,
378        shared: None,
379    }
380}