sv_sim_sim/state/event.rs
1//! STATE::EVENT — `event` variable ("trigger pulse") cells (R-run Task 16,
2//! moved from `2026-09-27-wave-B-state.md` Task 5; ledger ST-111).
3//!
4//! **Deviation, documented (ST-111's own acceptance text explicitly leaves
5//! the exact mechanism an "open joint decision with STATE/NETS" — this is
6//! STATE's chosen resolution, not a bilaterally closed one):** rather than
7//! adding a new [`super::SlotClass::Event`] variant (which would force a
8//! mechanical ripple through every exhaustive `SlotClass` match in
9//! `write.rs`/`rollback.rs`/`accessor_impl.rs`/`dedup.rs`/`instance.rs`, the
10//! same shape Task 1 already does for row-addressing but for no actual
11//! storage-shape benefit — an event has no persistent value at all), an
12//! event variable is an ordinary 1-bit `SlotClass::Inline` cell whose
13//! [`StateCellId`] is additionally tracked in a dedicated
14//! `StateEngine::event_cells` set. This satisfies "a `StateCellId`-keyed
15//! mechanism" literally, keeps every existing exhaustive match unchanged,
16//! and is exactly as testable.
17//!
18//! Names no `sv_lang`/`sv_lang_kinds` item — reachable with or without this
19//! crate's `frontend` feature (CONTRACTS D4), matching `sched::EventTerm`/
20//! `sched::SensitivityTable`, the scheduler-side consumers of this cell.
21
22use super::write::{ChangeResult, PreWriteHook, SlotLocation};
23use super::{FourStateValue, SignalAccess, SlotClass, StateCellId};
24use crate::state::instance::StateEngine;
25
26/// Width of a design-declared `event` variable's cell (`state::layout`,
27/// `sv-sim-bin::run::signal_slots_of`) -- a 2-state `SlotClass::Wide` cell
28/// that compiled code (`sv-sim-codegen::expr::lower_event_trigger`) treats
29/// as two 64-bit halves: the low half COUNTS triggers, the high half holds
30/// `tick + 1` of the most recent trigger (0 = never), which is what
31/// `e.triggered` compares against the current time. Counting rather than
32/// toggling matters because blocking writes are flushed once at segment end:
33/// two toggles in one segment would cancel and report no change.
34///
35/// This is separate from the 1-bit [`StateEngine::alloc_event_cell`] model
36/// below, which the compiled pipeline does not use.
37pub const EVENT_CELL_WIDTH: u32 = 128;
38
39impl StateEngine {
40 /// ST-111: allocates a fresh event-variable cell — a real, ordinary
41 /// 1-bit `SlotClass::Inline` cell (via [`super::arena::Arena::alloc_inline_word`],
42 /// which zero-initializes its backing word, so the cell decodes to
43 /// logic `0`/"not triggered" immediately after allocation with no
44 /// further write needed) — and records its [`StateCellId`] in
45 /// `self.event_cells`, the one thing that distinguishes it from an
46 /// ordinary 1-bit variable cell (see this module's own doc comment for
47 /// why no new `SlotClass` variant exists for this).
48 pub fn alloc_event_cell(&mut self) -> StateCellId {
49 // SAFETY: `&mut self` guarantees no other call into this
50 // `StateEngine` (and therefore this `Arena`) races this allocation,
51 // and nothing in this crate holds a live `&[u128]`/`&mut [u128]`
52 // borrow of the inline plane across this call — the same
53 // single-logical-writer discipline every other `alloc_inline_word`
54 // call site in this crate (e.g. `allocate_instance`) already relies
55 // on.
56 let word_offset = unsafe { self.arena.alloc_inline_word() };
57 let loc = SlotLocation {
58 class: SlotClass::Inline,
59 word_offset,
60 bit_width: 1,
61 elem_word_count: 1,
62 row_stride_words: 0,
63 num_rows: 0,
64 };
65 let cell = self.alloc_cell(loc);
66 self.event_cells.insert(cell);
67 cell
68 }
69
70 /// ST-111: the trigger pulse — an ordinary [`StateEngine::commit_write`]
71 /// (so the real VC/`notify_change`/`Dispatch::notify_scheduler` path
72 /// fires, which is exactly what wakes a process sensitive to
73 /// `sched::EventTerm::NamedEventTrigger(cell)` via
74 /// `sched::SensitivityTable`'s already-landed "any real change wakes
75 /// it" handling — see `sched::sensitivity::SensitivityTable::install_term`'s
76 /// `NamedEventTrigger` arm). Writes the logical complement of the cell's
77 /// current value (an event cell's own "value" carries no LRM meaning —
78 /// only the transition matters — so flipping it, rather than always
79 /// writing the same constant, guarantees `commit_write`'s own
80 /// old-vs-new diff always reports a real change, even across repeated
81 /// triggers with no intervening clear).
82 ///
83 /// # Panics
84 /// In debug builds, if `cell` was never allocated via
85 /// [`Self::alloc_event_cell`] — a caller triggering an ordinary
86 /// (non-event) cell through this path is a programming error, not a
87 /// silently-tolerated case, matching this crate's "loud, never silent"
88 /// discipline (ARCHITECTURE §7 item 9).
89 pub fn trigger_event(
90 &mut self,
91 cell: StateCellId,
92 hook: &mut dyn PreWriteHook,
93 ) -> ChangeResult {
94 debug_assert!(
95 self.event_cells.contains(&cell),
96 "trigger_event: {cell:?} was never allocated via StateEngine::alloc_event_cell -- \
97 an ordinary variable/net cell has no LRM 'trigger' semantics to fire"
98 );
99 let current = match self.read(cell) {
100 FourStateValue::Narrow(word) => (word as u64) & 1 != 0,
101 FourStateValue::Wide { .. } => unreachable!(
102 "event cell {cell:?} decoded as FourStateValue::Wide -- \
103 alloc_event_cell only ever allocates SlotClass::Inline cells"
104 ),
105 };
106 let next = FourStateValue::Narrow(if current { 0u128 } else { 1u128 });
107 self.commit_write(cell, next, hook)
108 }
109
110 /// ST-111: LRM `.triggered`'s "valid only through the current time
111 /// step" — clears every tracked event cell back to `0`, via
112 /// [`StateEngine::commit_write_no_hook`] (no `VcRecord` append, no
113 /// scheduler notification on the clearing edge: clearing is not itself
114 /// an LRM-observable event — see [`Self::commit_write_no_hook`]'s own
115 /// doc comment for why that primitive is the one that skips both).
116 pub fn clear_event_pulses(&mut self) {
117 // Collect first: `commit_write_no_hook` takes `&mut self`, so this
118 // avoids borrowing `self.event_cells` and `self` mutably at once.
119 let cells: Vec<StateCellId> = self.event_cells.iter().copied().collect();
120 for cell in cells {
121 self.commit_write_no_hook(cell, FourStateValue::Narrow(0u128));
122 }
123 }
124}
125
126#[cfg(test)]
127mod tests {
128 use super::*;
129 use crate::state::instance::TemplateRegistry;
130
131 fn fresh_engine() -> StateEngine {
132 StateEngine::new(TemplateRegistry::new())
133 }
134
135 #[test]
136 fn alloc_event_cell_starts_at_zero_and_is_tracked() {
137 let mut engine = fresh_engine();
138 let ev = engine.alloc_event_cell();
139 assert!(engine.event_cells.contains(&ev));
140 match engine.read(ev) {
141 FourStateValue::Narrow(w) => assert_eq!(w as u64 & 1, 0),
142 FourStateValue::Wide { .. } => panic!("expected Narrow"),
143 }
144 }
145
146 #[test]
147 fn trigger_event_flips_the_cell_and_reports_a_change() {
148 let mut engine = fresh_engine();
149 let ev = engine.alloc_event_cell();
150 let result = engine.trigger_event(ev, &mut ());
151 assert!(result.changed);
152 match engine.read(ev) {
153 FourStateValue::Narrow(w) => assert_eq!(w as u64 & 1, 1),
154 FourStateValue::Wide { .. } => panic!("expected Narrow"),
155 }
156 }
157
158 /// Mutation-testing regression (2026-09-28): `trigger_event` must write
159 /// the logical COMPLEMENT of the cell's current value, never a constant
160 /// -- a mutant that always writes `1` still passes the single-trigger
161 /// test above (a fresh cell starts at `0`, so writing a constant `1` is
162 /// indistinguishable from flipping it), but silently drops the wake on
163 /// every SECOND consecutive trigger with no intervening
164 /// `clear_event_pulses` in between: `commit_write`'s own before/after
165 /// diff sees `1 -> 1` (no change), so `sched::sensitivity`'s "any real
166 /// change wakes it" mechanism this function's own doc comment describes
167 /// never fires on that second trigger. A real `-> ev;` statement inside
168 /// a tight loop with no intervening `@(ev)`/`.triggered` clear is exactly
169 /// this shape (LRM 1800-2023 SS15.5.1 places no such restriction on how
170 /// often `->` may fire).
171 #[test]
172 fn trigger_event_reports_a_change_on_every_consecutive_trigger_with_no_intervening_clear() {
173 let mut engine = fresh_engine();
174 let ev = engine.alloc_event_cell();
175
176 let first = engine.trigger_event(ev, &mut ());
177 assert!(first.changed, "first trigger (0 -> 1) must report a change");
178
179 let second = engine.trigger_event(ev, &mut ());
180 assert!(
181 second.changed,
182 "second consecutive trigger with no intervening clear must ALSO report a change \
183 (it must flip 1 -> 0, never re-write the same value 1 -> 1)"
184 );
185 match engine.read(ev) {
186 FourStateValue::Narrow(w) => assert_eq!(
187 w as u64 & 1,
188 0,
189 "the second trigger must flip the cell back to 0"
190 ),
191 FourStateValue::Wide { .. } => panic!("expected Narrow"),
192 }
193
194 let third = engine.trigger_event(ev, &mut ());
195 assert!(
196 third.changed,
197 "a third consecutive trigger must flip 0 -> 1 and report a change again"
198 );
199 }
200
201 #[test]
202 fn clear_event_pulses_resets_every_tracked_cell_to_zero() {
203 let mut engine = fresh_engine();
204 let a = engine.alloc_event_cell();
205 let b = engine.alloc_event_cell();
206 engine.trigger_event(a, &mut ());
207 engine.trigger_event(b, &mut ());
208 engine.clear_event_pulses();
209 for cell in [a, b] {
210 match engine.read(cell) {
211 FourStateValue::Narrow(w) => assert_eq!(w as u64 & 1, 0),
212 FourStateValue::Wide { .. } => panic!("expected Narrow"),
213 }
214 }
215 }
216}