subetha_core/axis_signature.rs
1//! `AxisSignature` - direction-signature catalog for the SubEtha
2//! design cube, spanning both the concurrent-data-structure domain
3//! (deque variants) and the exotic-pointer domain.
4//!
5//! Every variant in the MMF-deque family AND every exotic pointer
6//! type has a constrained **direction signature**: the set of axis
7//! values it engages at a non-default value. The dispatcher routes
8//! per call by satisfying the workload's required signature against
9//! the available variants' signatures.
10//!
11//! This module names the axes of the design cube and defines the
12//! `AxisMask` bitmask type that lets variants declare their
13//! engagement on each axis. The dispatcher tests
14//! `provided.satisfies(required)` to pick a routable variant.
15//!
16//! ## The deque-domain axes (bits 0..=5)
17//!
18//! 1. **K_inner**: items per slot. 1 or 3.
19//! 2. **K_outer**: slots per producer-counter atomic. 1 or K.
20//! 3. **K_consumer**: mailboxes per thief. shared or N per-thief.
21//! 4. **K_counter_share**: producer counter ownership. shared or owner-private.
22//! 5. **K_radius**: coherence distance of publish. Local or Distant (CPUID-dispatched).
23//! 6. **K_gating**: synchronisation granularity. counter-only or per-slot.
24//!
25//! ## The pointer-domain axes (bits 6..=12)
26//!
27//! 7. **K_stride**: stride encoded in shift count (kstep-style).
28//! 8. **K_segmented**: multi-segment address space (k-tower-style).
29//! 9. **K_content_prefix**: content-summary stored at slot (umbra,
30//! bloom, cardinality).
31//! 10. **K_type_tag**: type discriminant stored at slot (self-desc).
32//! 11. **K_version**: version metadata stored at slot.
33//! 12. **K_async**: future / async-state stored at slot.
34//! 13. **K_bounds**: runtime bounds metadata at slot (cheri-style).
35
36#![allow(clippy::missing_errors_doc)]
37
38use core::fmt;
39
40/// The axes of the SubEtha design cube. Bits 0..=5 are the
41/// concurrent-data-structure (deque) axes; bits 6..=12 are the
42/// exotic-pointer axes.
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
44pub enum Axis {
45 /// Items per slot (K_inner). Deque-domain.
46 Inner,
47 /// Slots per producer-counter atomic (K_outer). Deque-domain.
48 Outer,
49 /// Mailboxes per thief (K_consumer). Deque-domain.
50 Consumer,
51 /// Producer counter ownership (K_counter_share). Deque-domain.
52 CounterShare,
53 /// Coherence distance of publish (K_radius). Deque-domain.
54 Radius,
55 /// Per-slot atomic vs counter-only (K_gating). Deque-domain.
56 Gating,
57 /// Stride encoded in shift count (K_stride). Pointer-domain.
58 Stride,
59 /// Multi-segment address space (K_segmented). Pointer-domain.
60 Segmented,
61 /// Content-summary stored at slot (K_content_prefix).
62 /// Pointer-domain. Engaged by umbra / bloom / cardinality.
63 ContentPrefix,
64 /// Type discriminant stored at slot (K_type_tag). Pointer-domain.
65 TypeTag,
66 /// Version metadata stored at slot (K_version). Pointer-domain.
67 Version,
68 /// Future / async-state stored at slot (K_async). Pointer-domain.
69 Async,
70 /// Runtime bounds metadata stored at slot (K_bounds).
71 /// Pointer-domain.
72 Bounds,
73}
74
75impl Axis {
76 /// All axes in canonical order.
77 pub const ALL: [Axis; 13] = [
78 Axis::Inner,
79 Axis::Outer,
80 Axis::Consumer,
81 Axis::CounterShare,
82 Axis::Radius,
83 Axis::Gating,
84 Axis::Stride,
85 Axis::Segmented,
86 Axis::ContentPrefix,
87 Axis::TypeTag,
88 Axis::Version,
89 Axis::Async,
90 Axis::Bounds,
91 ];
92
93 /// Bit position of this axis in the packed `AxisMask` `u16`
94 /// representation. Bits 0..=5 are deque-domain; bits 6..=12 are
95 /// pointer-domain.
96 #[inline(always)]
97 pub const fn bit(self) -> u16 {
98 match self {
99 Axis::Inner => 0,
100 Axis::Outer => 1,
101 Axis::Consumer => 2,
102 Axis::CounterShare => 3,
103 Axis::Radius => 4,
104 Axis::Gating => 5,
105 Axis::Stride => 6,
106 Axis::Segmented => 7,
107 Axis::ContentPrefix => 8,
108 Axis::TypeTag => 9,
109 Axis::Version => 10,
110 Axis::Async => 11,
111 Axis::Bounds => 12,
112 }
113 }
114}
115
116impl fmt::Display for Axis {
117 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118 let name = match self {
119 Axis::Inner => "K_inner",
120 Axis::Outer => "K_outer",
121 Axis::Consumer => "K_consumer",
122 Axis::CounterShare => "K_counter_share",
123 Axis::Radius => "K_radius",
124 Axis::Gating => "K_gating",
125 Axis::Stride => "K_stride",
126 Axis::Segmented => "K_segmented",
127 Axis::ContentPrefix => "K_content_prefix",
128 Axis::TypeTag => "K_type_tag",
129 Axis::Version => "K_version",
130 Axis::Async => "K_async",
131 Axis::Bounds => "K_bounds",
132 };
133 f.write_str(name)
134 }
135}
136
137/// A bitmask over the design-cube axes. Bit `i` set means the
138/// corresponding axis is engaged at its non-default value.
139///
140/// The direction signature for a variant is its `AxisMask`: which
141/// axes it sets to non-default values. A workload's required
142/// signature is also an `AxisMask`: which axes it needs the
143/// transport (or pointer type) to handle non-trivially.
144#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
145pub struct AxisMask(u16);
146
147impl AxisMask {
148 /// The mask covering every defined axis. Bits 0..=12 are valid.
149 const VALID_BITS: u16 = (1u16 << 13) - 1;
150
151 /// The empty signature (no axes engaged). Corresponds to the
152 /// origin corner of the cube (Chase-Lev's signature).
153 pub const EMPTY: AxisMask = AxisMask(0);
154
155 /// All defined axes engaged.
156 pub const ALL: AxisMask = AxisMask(Self::VALID_BITS);
157
158 /// Build a signature from a slice of engaged axes.
159 pub const fn from_axes(axes: &[Axis]) -> Self {
160 let mut bits = 0u16;
161 let mut i = 0;
162 while i < axes.len() {
163 bits |= 1u16 << axes[i].bit();
164 i += 1;
165 }
166 AxisMask(bits)
167 }
168
169 /// Build from a raw `u16`. Bits outside the valid range are
170 /// masked off.
171 pub const fn from_bits(bits: u16) -> Self {
172 AxisMask(bits & Self::VALID_BITS)
173 }
174
175 /// Return the raw `u16` representation.
176 #[inline(always)]
177 pub const fn bits(self) -> u16 {
178 self.0
179 }
180
181 /// `true` if axis `a` is engaged in this signature.
182 #[inline(always)]
183 pub const fn contains(self, a: Axis) -> bool {
184 (self.0 >> a.bit()) & 1 == 1
185 }
186
187 /// Count of engaged axes (popcount of the bitmask).
188 #[inline(always)]
189 pub const fn count(self) -> u32 {
190 self.0.count_ones()
191 }
192
193 /// Union of two signatures.
194 #[inline(always)]
195 pub const fn union(self, other: AxisMask) -> AxisMask {
196 AxisMask(self.0 | other.0)
197 }
198
199 /// Intersection of two signatures.
200 #[inline(always)]
201 pub const fn intersection(self, other: AxisMask) -> AxisMask {
202 AxisMask(self.0 & other.0)
203 }
204
205 /// `true` if this signature is a superset of `other` (i.e. every
206 /// axis required by `other` is engaged in `self`).
207 ///
208 /// `provided.satisfies(required)` means the variant `provided`
209 /// can transport the workload `required`.
210 #[inline(always)]
211 pub const fn satisfies(self, other: AxisMask) -> bool {
212 (self.0 & other.0) == other.0
213 }
214
215 /// Hamming distance between two signatures: how many axes
216 /// differ. This is the cube-distance the dispatcher pays when
217 /// routing a workload to a non-perfect-match variant.
218 #[inline(always)]
219 pub const fn distance(self, other: AxisMask) -> u32 {
220 (self.0 ^ other.0).count_ones()
221 }
222}
223
224impl fmt::Display for AxisMask {
225 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
226 let mut first = true;
227 f.write_str("{")?;
228 for a in Axis::ALL {
229 if self.contains(a) {
230 if !first {
231 f.write_str(", ")?;
232 }
233 fmt::Display::fmt(&a, f)?;
234 first = false;
235 }
236 }
237 f.write_str("}")
238 }
239}
240
241/// Cross-axis fusions a variant implements. Each fusion is an
242/// `AxisMask` of two or three axes whose state is packed into one
243/// atomic word (for ≤8 B fusion) or one 64-byte cache-line publish
244/// (for MOVDIR64B-based corner fusion).
245///
246/// Terminology: a 2-axis fusion packs two axes' state into one
247/// atomic word; a 3-axis fusion publishes three axes' state in one
248/// instruction; a 4-axis fusion is the maximum on a single cache
249/// line.
250#[derive(Debug, Clone, Copy, PartialEq, Eq)]
251pub struct Fusion {
252 /// The axes packed into the fused atomic word.
253 pub axes: AxisMask,
254}
255
256impl Fusion {
257 /// Build a 2-axis fusion.
258 pub const fn pair(a: Axis, b: Axis) -> Self {
259 Self {
260 axes: AxisMask::from_axes(&[a, b]),
261 }
262 }
263
264 /// Build a 3-axis fusion.
265 pub const fn triple(a: Axis, b: Axis, c: Axis) -> Self {
266 Self {
267 axes: AxisMask::from_axes(&[a, b, c]),
268 }
269 }
270
271 /// Number of axes engaged in this fusion (2 for pairs, 3 for
272 /// triples, etc.).
273 #[inline(always)]
274 pub const fn axis_count(self) -> u32 {
275 self.axes.count()
276 }
277}
278
279#[cfg(test)]
280mod tests {
281 use super::*;
282
283 #[test]
284 fn empty_signature_contains_nothing() {
285 assert!(!AxisMask::EMPTY.contains(Axis::Inner));
286 assert_eq!(AxisMask::EMPTY.count(), 0);
287 }
288
289 #[test]
290 fn all_signature_contains_everything() {
291 for a in Axis::ALL {
292 assert!(AxisMask::ALL.contains(a));
293 }
294 assert_eq!(AxisMask::ALL.count(), Axis::ALL.len() as u32);
295 }
296
297 #[test]
298 fn from_axes_packs_bits_correctly() {
299 let s = AxisMask::from_axes(&[Axis::Inner, Axis::Gating]);
300 assert!(s.contains(Axis::Inner));
301 assert!(s.contains(Axis::Gating));
302 assert!(!s.contains(Axis::Outer));
303 assert_eq!(s.count(), 2);
304 }
305
306 #[test]
307 fn satisfies_is_superset_check() {
308 let chase_lev = AxisMask::EMPTY;
309 let khl = AxisMask::from_axes(&[
310 Axis::Inner,
311 Axis::Outer,
312 Axis::CounterShare,
313 Axis::Radius,
314 Axis::Gating,
315 ]);
316 let request_reply = AxisMask::EMPTY;
317 let producer_fast = AxisMask::from_axes(&[Axis::Inner, Axis::Outer]);
318
319 assert!(chase_lev.satisfies(request_reply));
320 assert!(khl.satisfies(request_reply));
321 assert!(khl.satisfies(producer_fast));
322 assert!(!chase_lev.satisfies(producer_fast));
323 }
324
325 #[test]
326 fn distance_counts_differing_axes() {
327 let a = AxisMask::from_axes(&[Axis::Inner, Axis::Outer]);
328 let b = AxisMask::from_axes(&[Axis::Inner, Axis::Gating]);
329 // Outer and Gating differ; Inner agrees.
330 assert_eq!(a.distance(b), 2);
331 assert_eq!(a.distance(a), 0);
332 assert_eq!(
333 AxisMask::EMPTY.distance(AxisMask::ALL),
334 Axis::ALL.len() as u32
335 );
336 }
337
338 #[test]
339 fn pair_fusion_has_axis_count_two() {
340 let f = Fusion::pair(Axis::Inner, Axis::Gating);
341 assert_eq!(f.axis_count(), 2);
342 }
343
344 #[test]
345 fn triple_fusion_has_axis_count_three() {
346 let f = Fusion::triple(Axis::Radius, Axis::Inner, Axis::Gating);
347 assert_eq!(f.axis_count(), 3);
348 }
349
350 #[test]
351 fn display_renders_axis_names() {
352 let s = AxisMask::from_axes(&[Axis::Inner, Axis::Radius]);
353 let rendered = format!("{s}");
354 assert!(rendered.contains("K_inner"));
355 assert!(rendered.contains("K_radius"));
356 }
357}