Skip to main content

fuel_dispatch/fkc/
caps_map.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Five-flag layout set → today's [`KernelCaps`] projection (adoption
3//! plan §6 / FKC §4.1, §12.2).
4//!
5//! FKC carries five independent layout facts per operand
6//! (`contiguous`, `strided`, `broadcast_stride0`, `start_offset`,
7//! `reverse_strides`); today's [`KernelCaps`] (`kernel.rs`) has exactly
8//! one bool, `strided_input`. This module parses each tri-state string
9//! to a typed [`Tri`] and projects per the **EXACT** §6 rule:
10//!
11//! ```text
12//! KernelCaps.strided_input = (strided == accepted) && (broadcast_stride0 == accepted)
13//! ```
14//!
15//! The other three flags are handled per as-built behavior:
16//! - `start_offset` is parsed + retained but **NOT** projected — a
17//!   non-zero `byte_offset` operand still routes through auto-Contiguize
18//!   today (`kernel.rs` doc-comment). [consumer-ahead].
19//! - `reverse_strides` is parsed + retained but **NOT** projected — the
20//!   `KernelCaps` flag does not exist yet; a negative-stride operand is
21//!   normalized by the planner until the field lands. [consumer-ahead].
22//! - `contiguous` is parsed + retained (coherence + forward use).
23//!
24//! The importer **retains** every parsed flag on a [`ResolvedLayout`] so
25//! nothing is lost; it emits only the `strided_input` projection today.
26//! The moment `KernelCaps` grows `reverse_strides` / `start_offset_capable`
27//! the retained values fill them (the forward-extension hook, §6).
28
29use crate::fkc::error::FkcError;
30use crate::fkc::schema::LayoutSpec;
31use crate::kernel::KernelCaps;
32
33/// A parsed layout tri-state (FKC §4.1). `contiguous` admits `Required`;
34/// the other four flags use only `Accepted` / `Rejected`. `NotApplicable`
35/// (`n/a`) is the "no constraint declared" value. An absent flag (YAML
36/// key omitted) defaults to [`Tri::Rejected`] for the capability flags
37/// (the conservative default matching today's all-false `KernelCaps`).
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum Tri {
40    /// `required` — the kernel demands this property of the operand.
41    Required,
42    /// `accepted` — the kernel handles this property directly.
43    Accepted,
44    /// `rejected` — the kernel cannot consume this property (planner
45    /// must normalize first).
46    Rejected,
47    /// `n/a` — no constraint declared.
48    NotApplicable,
49}
50
51impl Tri {
52    /// Parse one tri-state token. `flag`/`operand`/`section` give the
53    /// error context. An absent value (`None`) maps to `default`.
54    fn parse(
55        value: Option<&str>,
56        default: Tri,
57        section: &str,
58        operand: &str,
59        flag: &str,
60    ) -> Result<Tri, FkcError> {
61        match value {
62            None => Ok(default),
63            Some(v) => match v.trim() {
64                "required" => Ok(Tri::Required),
65                "accepted" => Ok(Tri::Accepted),
66                "rejected" => Ok(Tri::Rejected),
67                "n/a" | "na" => Ok(Tri::NotApplicable),
68                other => Err(FkcError::BadLayoutFlag {
69                    section: section.to_string(),
70                    operand: operand.to_string(),
71                    flag: flag.to_string(),
72                    value: other.to_string(),
73                }),
74            },
75        }
76    }
77
78    /// Whether this flag is `accepted` (the projection predicate).
79    pub fn is_accepted(self) -> bool {
80        matches!(self, Tri::Accepted)
81    }
82
83    /// Whether this flag is `required` (the operand MUST carry the property;
84    /// for `broadcast_stride0` this marks a baked-broadcast kernel).
85    pub fn is_required(self) -> bool {
86        matches!(self, Tri::Required)
87    }
88}
89
90/// The full typed five-flag layout set for one operand — every flag
91/// retained, even the ones not yet projected onto `KernelCaps` (§6
92/// [consumer-ahead]).
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub struct ResolvedLayout {
95    /// `required` | `accepted` | `n/a` for the contiguous property.
96    pub contiguous: Tri,
97    /// `accepted` | `rejected` — kernel walks explicit strides.
98    pub strided: Tri,
99    /// `accepted` | `rejected` — kernel handles a stride-0 broadcast axis.
100    pub broadcast_stride0: Tri,
101    /// `accepted` | `rejected` — kernel honors a non-zero start offset.
102    /// Retained, not projected (auto-Contiguize handles it today).
103    pub start_offset: Tri,
104    /// `accepted` | `rejected` — kernel walks negative (reverse) strides.
105    /// Retained, not projected (no `KernelCaps` field yet).
106    pub reverse_strides: Tri,
107}
108
109impl ResolvedLayout {
110    /// Project this operand's flags onto today's single-bool
111    /// [`KernelCaps`] per the EXACT §6 rule:
112    ///
113    /// `strided_input = (strided == accepted) && (broadcast_stride0 == accepted)`.
114    pub fn project(&self) -> KernelCaps {
115        let strided_input = self.strided.is_accepted() && self.broadcast_stride0.is_accepted();
116        // `broadcast_stride0: required` marks a baked-broadcast kernel: the
117        // realize pick must exclude it from a dense operand (path 1a). See
118        // [`KernelCaps::requires_broadcast`].
119        let requires_broadcast = self.broadcast_stride0.is_required();
120        KernelCaps {
121            strided_input,
122            requires_broadcast,
123        }
124    }
125}
126
127/// Parse one operand's [`LayoutSpec`] into a typed [`ResolvedLayout`].
128/// An absent `layout` block (the `None` case) is the conservative
129/// all-`rejected` set (matches today's default all-false `KernelCaps`).
130pub fn resolve_layout(
131    spec: Option<&LayoutSpec>,
132    section: &str,
133    operand: &str,
134) -> Result<ResolvedLayout, FkcError> {
135    let s = spec;
136    Ok(ResolvedLayout {
137        contiguous: Tri::parse(
138            s.and_then(|l| l.contiguous.as_deref()),
139            Tri::NotApplicable,
140            section,
141            operand,
142            "contiguous",
143        )?,
144        strided: Tri::parse(
145            s.and_then(|l| l.strided.as_deref()),
146            Tri::Rejected,
147            section,
148            operand,
149            "strided",
150        )?,
151        broadcast_stride0: Tri::parse(
152            s.and_then(|l| l.broadcast_stride0.as_deref()),
153            Tri::Rejected,
154            section,
155            operand,
156            "broadcast_stride0",
157        )?,
158        start_offset: Tri::parse(
159            s.and_then(|l| l.start_offset.as_deref()),
160            Tri::Rejected,
161            section,
162            operand,
163            "start_offset",
164        )?,
165        reverse_strides: Tri::parse(
166            s.and_then(|l| l.reverse_strides.as_deref()),
167            Tri::Rejected,
168            section,
169            operand,
170            "reverse_strides",
171        )?,
172    })
173}
174
175/// Project a whole kernel's per-operand layouts onto **one** kernel-level
176/// [`KernelCaps`]: a kernel can stride its inputs only if *every* input
177/// operand accepts the strided + broadcast properties. (The binding
178/// table carries one `KernelCaps` per kernel, not per operand; the
179/// conservative AND is the faithful single-bool collapse — if any operand
180/// must be contiguous, the kernel as a whole is not strided-capable.)
181///
182/// An empty operand list yields the all-false default.
183pub fn project_kernel_caps(operand_layouts: &[ResolvedLayout]) -> KernelCaps {
184    if operand_layouts.is_empty() {
185        return KernelCaps::empty();
186    }
187    let strided_input = operand_layouts.iter().all(|l| l.project().strided_input);
188    // If ANY operand must be broadcast (a baked stride-0), the KERNEL is a
189    // baked-broadcast kernel — excluded from generic (dense) selection.
190    let requires_broadcast = operand_layouts
191        .iter()
192        .any(|l| l.broadcast_stride0.is_required());
193    KernelCaps {
194        strided_input,
195        requires_broadcast,
196    }
197}
198
199/// Is this contract the GENERIC (fully-permissive strided) one — admissible
200/// anywhere because it imposes no structure tightness?
201///
202/// Reads the retained FKC five-flag layout set: a generic contract accepts
203/// strided + broadcast on **every** operand and requires contiguity on none.
204/// A structure-specialized contract requires contiguity on some operand (or,
205/// forward, a tighter vec/inner-div predicate the as-built five-flag set does
206/// not yet carry). An empty operand set is not generic (no contract to fall
207/// back to).
208///
209/// This is the classifier the Baracuda **miss** telemetry keys on (FKC §4.12):
210/// the FKC importer computes it once from a kernel's retained
211/// [`ResolvedLayout`] set at registration time and stamps the resulting bit
212/// onto its [`crate::kernel::BindingEntry`], so the live dispatch pick site
213/// reads a precomputed bool rather than re-deriving genericity from the lossy
214/// single-bool [`KernelCaps`]. It lives here (not in the feature-gated
215/// `telemetry` module) because genericity is a pure property of the
216/// always-compiled `ResolvedLayout`, and the always-compiled FKC importer must
217/// reach it without the `telemetry` feature. The `telemetry` module re-exports
218/// it for the detector's own use.
219pub fn is_generic_contract(layouts: &[ResolvedLayout]) -> bool {
220    !layouts.is_empty()
221        && layouts.iter().all(|l| {
222            l.strided.is_accepted()
223                && l.broadcast_stride0.is_accepted()
224                && l.contiguous != Tri::Required
225        })
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231
232    fn spec(
233        contiguous: Option<&str>,
234        strided: Option<&str>,
235        broadcast: Option<&str>,
236        start_offset: Option<&str>,
237        reverse: Option<&str>,
238    ) -> LayoutSpec {
239        LayoutSpec {
240            contiguous: contiguous.map(String::from),
241            strided: strided.map(String::from),
242            broadcast_stride0: broadcast.map(String::from),
243            broadcast_axes: None,
244            start_offset: start_offset.map(String::from),
245            reverse_strides: reverse.map(String::from),
246            awkward_layout_strategy: None,
247        }
248    }
249
250    fn resolve(s: &LayoutSpec) -> ResolvedLayout {
251        resolve_layout(Some(s), "test", "op").expect("layout resolves")
252    }
253
254    #[test]
255    fn required_broadcast_projects_requires_broadcast_not_strided() {
256        // `broadcast_stride0: required` marks a baked-broadcast kernel: the
257        // projection sets `requires_broadcast` (→ excluded from dense
258        // selection) and does NOT set `strided_input` (Required != Accepted).
259        let caps = resolve(&spec(
260            Some("accepted"),
261            Some("accepted"),
262            Some("required"),
263            Some("rejected"),
264            Some("rejected"),
265        ))
266        .project();
267        assert!(
268            caps.requires_broadcast,
269            "required broadcast → requires_broadcast"
270        );
271        assert!(
272            !caps.strided_input,
273            "Required is not Accepted → not strided-generic"
274        );
275
276        // A generic (broadcast_stride0: accepted) operand is dense-safe.
277        let generic = resolve(&spec(
278            Some("accepted"),
279            Some("accepted"),
280            Some("accepted"),
281            Some("rejected"),
282            Some("rejected"),
283        ))
284        .project();
285        assert!(
286            !generic.requires_broadcast,
287            "accepted broadcast is generic, not baked"
288        );
289        assert!(generic.strided_input);
290    }
291
292    #[test]
293    fn project_kernel_caps_requires_broadcast_if_any_operand_does() {
294        let baked = resolve(&spec(
295            Some("accepted"),
296            Some("accepted"),
297            Some("required"),
298            Some("rejected"),
299            Some("rejected"),
300        ));
301        let generic = resolve(&spec(
302            Some("accepted"),
303            Some("accepted"),
304            Some("accepted"),
305            Some("rejected"),
306            Some("rejected"),
307        ));
308        assert!(
309            project_kernel_caps(&[generic, baked]).requires_broadcast,
310            "any baked operand ⇒ the kernel is baked-broadcast",
311        );
312        assert!(
313            !project_kernel_caps(&[generic, generic]).requires_broadcast,
314            "all-generic ⇒ dense-safe",
315        );
316    }
317
318    #[test]
319    /// FKC-CLAUSE: FKC-4.3-0001 FKC-4.3-0002 — the layout tri-states project to
320    /// `strided_input`, which is what decides whether the planner inserts
321    /// `Op::Contiguize`. `requires_contiguous` (contiguous: required) projects
322    /// false -> planner inserts; `contiguize_internally`/`handles_strided`
323    /// (strided: accepted) project true -> planner must NOT insert a separate
324    /// Contiguize. Validator rule 5 enforces strategy/flag coherence, so the
325    /// declared strategy reaches the planner THROUGH these flags.
326    fn projection_truth_table() {
327        // strided accepted + broadcast accepted ⇒ strided_input true.
328        let r = resolve(&spec(
329            Some("accepted"),
330            Some("accepted"),
331            Some("accepted"),
332            Some("rejected"),
333            Some("rejected"),
334        ));
335        assert!(r.project().strided_input);
336
337        // strided rejected ⇒ false even if broadcast accepted.
338        let r = resolve(&spec(
339            Some("required"),
340            Some("rejected"),
341            Some("accepted"),
342            Some("rejected"),
343            Some("rejected"),
344        ));
345        assert!(!r.project().strided_input);
346
347        // broadcast rejected ⇒ false even if strided accepted.
348        let r = resolve(&spec(None, Some("accepted"), Some("rejected"), None, None));
349        assert!(!r.project().strided_input);
350
351        // both rejected ⇒ false.
352        let r = resolve(&spec(
353            Some("required"),
354            Some("rejected"),
355            Some("rejected"),
356            Some("rejected"),
357            Some("rejected"),
358        ));
359        assert!(!r.project().strided_input);
360    }
361
362    #[test]
363    fn start_offset_does_not_flip_strided_input() {
364        // start_offset accepted but strided rejected ⇒ still false
365        // (auto-Contiguize handles offset today; §6).
366        let r = resolve(&spec(
367            Some("required"),
368            Some("rejected"),
369            Some("rejected"),
370            Some("accepted"),
371            Some("rejected"),
372        ));
373        assert!(!r.project().strided_input);
374        // …but the parsed value is retained.
375        assert_eq!(r.start_offset, Tri::Accepted);
376    }
377
378    #[test]
379    fn reverse_strides_retained_not_projected() {
380        let r = resolve(&spec(
381            None,
382            Some("accepted"),
383            Some("accepted"),
384            None,
385            Some("accepted"),
386        ));
387        // strided_input still derived only from strided+broadcast.
388        assert!(r.project().strided_input);
389        // reverse_strides parsed + retained for forward use.
390        assert_eq!(r.reverse_strides, Tri::Accepted);
391    }
392
393    #[test]
394    fn elementwise_binary_contract_projection_is_false() {
395        // The real elementwise-binary layout: contiguous required, the
396        // rest rejected ⇒ strided_input false.
397        let r = resolve(&spec(
398            Some("required"),
399            Some("rejected"),
400            Some("rejected"),
401            Some("rejected"),
402            Some("rejected"),
403        ));
404        assert!(!r.project().strided_input);
405        assert_eq!(r.contiguous, Tri::Required);
406    }
407
408    #[test]
409    fn absent_layout_is_conservative_false() {
410        let r = resolve_layout(None, "test", "op").unwrap();
411        assert!(!r.project().strided_input);
412        assert_eq!(r.strided, Tri::Rejected);
413    }
414
415    #[test]
416    fn unknown_tristate_value_is_typed_error() {
417        let s = spec(Some("maybe"), None, None, None, None);
418        let err = resolve_layout(Some(&s), "test", "op").expect_err("bad value errors");
419        assert!(matches!(err, FkcError::BadLayoutFlag { .. }), "got {err:?}");
420    }
421
422    #[test]
423    fn kernel_caps_is_and_of_operands() {
424        let strided = resolve(&spec(None, Some("accepted"), Some("accepted"), None, None));
425        let contig = resolve(&spec(
426            Some("required"),
427            Some("rejected"),
428            Some("rejected"),
429            None,
430            None,
431        ));
432        // All strided ⇒ true.
433        assert!(project_kernel_caps(&[strided, strided]).strided_input);
434        // Mixed ⇒ false (conservative collapse).
435        assert!(!project_kernel_caps(&[strided, contig]).strided_input);
436    }
437
438    #[test]
439    fn is_generic_contract_classifies_permissive_vs_specialized() {
440        // Fully-permissive strided (accepts strided + broadcast, no contiguity
441        // demand) ⇒ generic.
442        let generic = resolve(&spec(
443            Some("n/a"),
444            Some("accepted"),
445            Some("accepted"),
446            None,
447            None,
448        ));
449        assert!(is_generic_contract(&[generic, generic]));
450        // A contiguity-requiring operand ⇒ specialized, even if a sibling is
451        // permissive (all operands must be permissive).
452        let contig = resolve(&spec(
453            Some("required"),
454            Some("rejected"),
455            Some("rejected"),
456            None,
457            None,
458        ));
459        assert!(!is_generic_contract(&[generic, contig]));
460        assert!(!is_generic_contract(&[contig, contig]));
461        // Empty is not generic (no contract to fall back to).
462        assert!(!is_generic_contract(&[]));
463    }
464
465    /// ⚠️ **THE FORWARD-EXTENSION HOOK'S CLOCK.**
466    ///
467    /// This module's own doc says, of the flags it parses but does not project:
468    ///
469    /// > *"The moment `KernelCaps` grows `reverse_strides` /
470    /// > `start_offset_capable` the retained values fill them (the
471    /// > forward-extension hook, §6)."*
472    ///
473    /// **That hook fires only if somebody goes.** `reverse_strides` is parsed,
474    /// retained and validated (it has its own `FkcError` arm) across ~1.9k
475    /// contract sites and is projected NOWHERE. If a `KernelCaps` field lands
476    /// and nobody wires the retained value, every one of those sites keeps
477    /// being parsed, retained and ignored — **with nothing red.** A deferral
478    /// whose trigger is *"when someone adds a field"* has no detector, because
479    /// adding the field is not an event anything watches.
480    ///
481    /// # Why a destructuring and not an assertion
482    ///
483    /// The obvious guard — *assert a negative stride implies a non-zero
484    /// `start_offset`* — would re-assert **planner behaviour that is already
485    /// documented twice** (this module's doc, and `KernelCaps::strided_input`'s
486    /// field doc in `kernel.rs`). **The unguarded thing is the HOOK, and you
487    /// guard a hook by making the field's ARRIVAL loud — not by restating what
488    /// the deferral defers to.**
489    ///
490    /// `KernelCaps` is deliberately not `#[non_exhaustive]` and its own doc
491    /// invites growth: *"Forward-extensible by adding fields (no enum/bitflags
492    /// churn)."* **So the struct invites exactly the change that silently
493    /// orphans the retained values, and this pattern fails to COMPILE on the
494    /// day it happens** — at the moment someone must decide whether the new
495    /// field belongs to the hook. `error[E0027]` names the field it is missing.
496    ///
497    /// # The field's arrival is NOT silent, and that CHANGES what this is for
498    ///
499    /// Measured: adding `reverse_strides: bool` to `KernelCaps` breaks the
500    /// build in SEVEN places before this test is even reached -- `E0063` at
501    /// `caps_map.rs` x2 and `kernel.rs` x5. **So "nothing goes red" is false
502    /// and this guard is not the only signal.**
503    ///
504    /// **What it adds is narrower and still worth having: `E0063` demands a
505    /// VALUE, not a DECISION.** The path of least resistance at those sites is
506    /// `reverse_strides: false` -- which compiles, is green, and silently
507    /// orphans every value the importer has been retaining. **The pre-existing
508    /// signal exists and points AWAY from the correct action.** This one fires
509    /// in the file that owns the deferral, under the sentence saying what the
510    /// retained values are for.
511    ///
512    /// AND RUSTC ITSELF SUGGESTS THE FIX THAT DISARMS THIS GUARD: `E0027`'s
513    /// third help is *"or always ignore missing fields here"*, with a `..`
514    /// rest pattern. **Taking it silently converts this test into one that can
515    /// never fail again.** That is why the exhaustive form is spelled out, and
516    /// why this paragraph exists rather than trusting the next reader to
517    /// notice.
518    ///
519    /// **If you are here because this stopped compiling:** decide whether the
520    /// new field is one the importer already retains a value for. If it is,
521    /// wire it in `layout_caps` and delete the deferral note above. If it is
522    /// not, add it to the pattern. **Do not delete this test to make the build
523    /// green** — its failure IS the notification.
524    ///
525    /// Shape borrowed from baracuda, who hit the identical problem on
526    /// `OpAttrs` and solved it the same way.
527    #[test]
528    fn kernel_caps_field_set_is_pinned_to_the_forward_extension_hook() {
529        // Exhaustive by construction: a `..` rest pattern here would defeat the
530        // entire point, and so would a field-count assertion — that is a
531        // hand-written list of the kind whose staleness this file is about.
532        let KernelCaps {
533            strided_input: _,
534            requires_broadcast: _,
535        } = KernelCaps::default();
536    }
537}