Skip to main content

vyre_primitives/effects/
handler_apply.rs

1//! Effect-handler application primitive (P-1.0-V1.1).
2//!
3//! Given an effect row (the side-effects a Region produces) and a
4//! handler (the set of effects it discharges), `handler_apply`
5//! returns the residual row. Composition of handlers is V1.2.
6//!
7//! The substrate handles a finite, ordered set of effect kinds via a
8//! u32 bitmask so the apply step is O(1) and lock-free.
9
10/// One concrete side-effect kind. Indexed by bit position into
11/// [`EffectRow`]. `#[non_exhaustive]` so future kinds (collective
12/// ops, persistent-storage writes, …) can land without breaking
13/// pattern matches.
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
15#[non_exhaustive]
16pub enum EffectKind {
17    /// A buffer write (Node::Store, Node::AsyncStore).
18    BufferWrite,
19    /// An atomic read-modify-write (Expr::Atomic).
20    Atomic,
21    /// A host-visible I/O effect (e.g. printk via host bridge).
22    HostIo,
23    /// A nested GPU dispatch (Node::IndirectDispatch).
24    GpuDispatch,
25    /// A barrier or synchronization primitive (Node::Barrier { ordering: vyre_foundation::memory_model::MemoryOrdering::SeqCst }).
26    Barrier,
27    /// An async-load fetching from persistent / streaming storage
28    /// (Node::AsyncLoad).
29    AsyncLoad,
30    /// A trap or abort (Node::Trap).
31    Trap,
32}
33
34impl EffectKind {
35    /// Bit position in an [`EffectRow`].
36    #[must_use]
37    #[inline]
38    pub const fn bit(self) -> u32 {
39        match self {
40            Self::BufferWrite => 0,
41            Self::Atomic => 1,
42            Self::HostIo => 2,
43            Self::GpuDispatch => 3,
44            Self::Barrier => 4,
45            Self::AsyncLoad => 5,
46            Self::Trap => 6,
47        }
48    }
49
50    /// Mask with this single bit set.
51    #[must_use]
52    #[inline]
53    pub const fn mask(self) -> u32 {
54        1u32 << self.bit()
55    }
56}
57
58/// Set of effect kinds produced by a Region. A row is a u32 bitmask
59/// indexed by `EffectKind::bit()`.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
61pub struct EffectRow(u32);
62
63impl EffectRow {
64    /// Empty row (no effects).
65    #[must_use]
66    #[inline]
67    pub const fn empty() -> Self {
68        Self(0)
69    }
70
71    /// Row from a raw u32 bitmask.
72    #[must_use]
73    #[inline]
74    pub const fn from_bits(bits: u32) -> Self {
75        Self(bits)
76    }
77
78    /// Row containing exactly one effect kind.
79    #[must_use]
80    #[inline]
81    pub const fn single(kind: EffectKind) -> Self {
82        Self(kind.mask())
83    }
84
85    /// Raw u32 bitmask.
86    #[must_use]
87    #[inline]
88    pub const fn bits(self) -> u32 {
89        self.0
90    }
91
92    /// Whether the row contains the given kind.
93    #[must_use]
94    #[inline]
95    pub const fn contains(self, kind: EffectKind) -> bool {
96        self.0 & kind.mask() != 0
97    }
98
99    /// Whether the row is empty.
100    #[must_use]
101    #[inline]
102    pub const fn is_empty(self) -> bool {
103        self.0 == 0
104    }
105
106    /// Set-union of two rows.
107    #[must_use]
108    #[inline]
109    pub const fn union(self, other: Self) -> Self {
110        Self(self.0 | other.0)
111    }
112}
113
114/// A handler discharges a fixed set of effect kinds. Modeled as the
115/// row of effects it consumes.
116#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
117pub struct Handler {
118    handled: EffectRow,
119}
120
121impl Handler {
122    /// Build a handler from the row of effects it discharges.
123    #[must_use]
124    #[inline]
125    pub const fn from_row(handled: EffectRow) -> Self {
126        Self { handled }
127    }
128
129    /// Single-effect handler.
130    #[must_use]
131    #[inline]
132    pub const fn single(kind: EffectKind) -> Self {
133        Self {
134            handled: EffectRow::single(kind),
135        }
136    }
137
138    /// The row of effects this handler discharges.
139    #[must_use]
140    #[inline]
141    pub const fn handled(self) -> EffectRow {
142        self.handled
143    }
144}
145
146/// Apply `handler` to `row` and return the residual row of open effects.
147///
148/// Algebraic identity: `handler_apply(handler_apply(row, h), h) ==
149/// handler_apply(row, h)` (idempotent), and
150/// `handler_apply(row, Handler::from_row(EffectRow::empty())) == row`
151/// (identity handler).
152#[must_use]
153#[inline]
154pub const fn handler_apply(row: EffectRow, handler: Handler) -> EffectRow {
155    EffectRow(row.0 & !handler.handled.0)
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn empty_row_stays_empty() {
164        let h = Handler::single(EffectKind::BufferWrite);
165        assert_eq!(handler_apply(EffectRow::empty(), h), EffectRow::empty());
166    }
167
168    #[test]
169    fn handler_discharges_its_kind() {
170        let row = EffectRow::single(EffectKind::BufferWrite);
171        let h = Handler::single(EffectKind::BufferWrite);
172        assert!(handler_apply(row, h).is_empty());
173    }
174
175    #[test]
176    fn handler_passes_through_other_kinds() {
177        let row = EffectRow::single(EffectKind::Atomic);
178        let h = Handler::single(EffectKind::BufferWrite);
179        assert_eq!(handler_apply(row, h).bits(), EffectKind::Atomic.mask());
180    }
181
182    #[test]
183    fn identity_handler_preserves_every_row() {
184        let id = Handler::from_row(EffectRow::empty());
185        for kind in [
186            EffectKind::BufferWrite,
187            EffectKind::Atomic,
188            EffectKind::HostIo,
189            EffectKind::GpuDispatch,
190            EffectKind::Barrier,
191            EffectKind::AsyncLoad,
192            EffectKind::Trap,
193        ] {
194            let row = EffectRow::single(kind);
195            assert_eq!(handler_apply(row, id).bits(), kind.mask());
196        }
197    }
198
199    #[test]
200    fn handler_apply_is_idempotent() {
201        let row =
202            EffectRow::single(EffectKind::BufferWrite).union(EffectRow::single(EffectKind::Atomic));
203        let h = Handler::single(EffectKind::BufferWrite);
204        let once = handler_apply(row, h);
205        let twice = handler_apply(once, h);
206        assert_eq!(once, twice);
207    }
208
209    #[test]
210    fn multi_effect_row_partial_discharge() {
211        let row =
212            EffectRow::single(EffectKind::BufferWrite).union(EffectRow::single(EffectKind::Atomic));
213        let h = Handler::single(EffectKind::BufferWrite);
214        let residual = handler_apply(row, h);
215        assert!(!residual.contains(EffectKind::BufferWrite));
216        assert!(residual.contains(EffectKind::Atomic));
217    }
218
219    #[test]
220    fn full_handler_discharges_full_row() {
221        let row = EffectRow::single(EffectKind::BufferWrite)
222            .union(EffectRow::single(EffectKind::Atomic))
223            .union(EffectRow::single(EffectKind::HostIo));
224        let h = Handler::from_row(row);
225        assert!(handler_apply(row, h).is_empty());
226    }
227
228    #[test]
229    fn distinct_kinds_have_distinct_bits() {
230        let bits: Vec<u32> = [
231            EffectKind::BufferWrite,
232            EffectKind::Atomic,
233            EffectKind::HostIo,
234            EffectKind::GpuDispatch,
235            EffectKind::Barrier,
236            EffectKind::AsyncLoad,
237            EffectKind::Trap,
238        ]
239        .iter()
240        .map(|k| k.bit())
241        .collect();
242        for i in 0..bits.len() {
243            for j in (i + 1)..bits.len() {
244                assert_ne!(bits[i], bits[j], "kinds {i} and {j} share a bit");
245            }
246        }
247    }
248
249    #[test]
250    fn from_bits_round_trip() {
251        // Bits 0, 1, 3, 5 set = BufferWrite + Atomic + GpuDispatch + AsyncLoad.
252        // Bit positions per `EffectKind::bit()`: BufferWrite=0, Atomic=1,
253        // HostIo=2, GpuDispatch=3, Barrier=4, AsyncLoad=5, Trap=6.
254        let raw = 0b0010_1011u32;
255        let row = EffectRow::from_bits(raw);
256        assert_eq!(row.bits(), raw);
257        assert!(row.contains(EffectKind::BufferWrite));
258        assert!(row.contains(EffectKind::Atomic));
259        assert!(!row.contains(EffectKind::HostIo));
260        assert!(row.contains(EffectKind::GpuDispatch));
261        assert!(!row.contains(EffectKind::Barrier));
262        assert!(row.contains(EffectKind::AsyncLoad));
263        assert!(!row.contains(EffectKind::Trap));
264    }
265}