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}