Skip to main content

fuel_dispatch/fkc/
error.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! FKC importer error type.
3//!
4//! Every parse / §3.8-enforcement / structural failure surfaces as a typed
5//! [`FkcError`] variant — the importer never panics on a production path
6//! (constitution: never-panic; FKC G9, §10). Each variant carries enough
7//! context (section name, line number where the YAML layer can provide it) to
8//! be actionable.
9//!
10//! This first slice covers the **parse + restricted-YAML (§3.8) + schema**
11//! failure modes. The lowering / validation / registration variants the full
12//! `V-FKC-*` battery needs (`UnknownOpKind`, `DuplicateKernelRef`,
13//! `ScaleDoubleDeclared`, `ShapeRuleMismatch`, …) land with their respective
14//! steps; placeholders for the prose-vs-structured blurb check (§10.11) are
15//! provided here so the variant exists ahead of its consumer.
16
17use thiserror::Error;
18
19/// A typed FKC import failure (`Result`-returning, never a panic).
20#[derive(Debug, Error)]
21#[non_exhaustive]
22pub enum FkcError {
23    // ===== §3.8 restricted-YAML pre-pass =====
24    /// Tab indentation inside a YAML chunk (front-matter or an fkc block).
25    /// YAML forbids tab indentation; §3.8 makes this a hard error with a line
26    /// rather than a confusing downstream parse error.
27    #[error("FKC §3.8: tab indentation is forbidden (line {line})")]
28    TabIndentation { line: usize },
29
30    /// A YAML anchor (`&name`) was used. §3.8 disables anchors so a diff
31    /// reviewer sees every value literally and the importer never resolves a
32    /// reference graph.
33    #[error("FKC §3.8: YAML anchors (`&name`) are disallowed (line {line})")]
34    AnchorDisallowed { line: usize },
35
36    /// A YAML alias (`*name`) was used. Disallowed for the same reason as
37    /// anchors (§3.8).
38    #[error("FKC §3.8: YAML aliases (`*name`) are disallowed (line {line})")]
39    AliasDisallowed { line: usize },
40
41    /// A YAML merge key (`<<:`) was used. Disallowed (§3.8).
42    #[error("FKC §3.8: YAML merge keys (`<<:`) are disallowed (line {line})")]
43    MergeKeyDisallowed { line: usize },
44
45    /// An unquoted Norway-problem token (`no`/`yes`/`on`/`off`/`n`/`y`) appeared
46    /// in a scalar value position. §3.8 disarms the Norway problem by requiring
47    /// such tokens to be quoted so they cannot silently coerce to a bool.
48    #[error(
49        "FKC §3.8: unquoted token `{token}` in a scalar position would coerce to a bool under \
50         YAML 1.1 (the Norway problem); quote it (line {line})"
51    )]
52    NorwayToken { token: String, line: usize },
53
54    // ===== markdown / file anatomy (§3.1) =====
55    /// The file-level `---`-fenced YAML front-matter is missing or malformed.
56    #[error("FKC §3.1: malformed or missing front-matter: {0}")]
57    MalformedFrontMatter(String),
58
59    /// A `## ` kernel section has no fenced ` ```fkc ` block (§3.1 requires
60    /// exactly one).
61    #[error("FKC §3.1: section `{section}` has no ```fkc block")]
62    MissingFkcBlock { section: String },
63
64    /// A `## ` kernel section has more than one fenced ` ```fkc ` block (§3.1
65    /// requires exactly one).
66    #[error("FKC §3.1: section `{section}` has {count} ```fkc blocks (expected exactly 1)")]
67    MultipleFkcBlocks { section: String, count: usize },
68
69    /// A `` ```fkc `` fenced block appears outside any `## ` section (before the
70    /// first heading, or in a file with no `## ` headings). Such a block is
71    /// silently dropped by section scanning, so the import would succeed while
72    /// adopting nothing — a no-op that looks like success (§3.1).
73    #[error(
74        "FKC §3.1: a ```fkc block (line {line}) is not under a `## ` heading — every kernel block \
75         must belong to a `## ` section (a block outside a section would be silently ignored)"
76    )]
77    OrphanFkcBlock { line: usize },
78
79    // ===== deserialization / schema =====
80    /// `serde_yaml_ng` failed to deserialize a YAML chunk into the schema. The
81    /// string carries the underlying error (and the section name when known).
82    #[error("FKC: YAML deserialize error: {0}")]
83    Yaml(String),
84
85    /// A required schema field is absent (V-FKC-1, §10). `field` names the
86    /// missing key; `section` the kernel section it belongs to.
87    #[error("FKC §10 (V-FKC-1): kernel `{section}` is missing required field `{field}`")]
88    MissingRequiredField { section: String, field: String },
89
90    // ===== §10.11 blurb equality (variant reserved ahead of its consumer) =====
91    /// The structured `blurb:` does not equal the prose blurb (V-FKC-2,
92    /// §10.11). Reserved here; the prose-vs-structured comparison is wired in a
93    /// later step.
94    #[error(
95        "FKC §10.11 (V-FKC-2): kernel `{section}` structured blurb does not match prose blurb \
96         (structured={structured:?}, prose={prose:?})"
97    )]
98    BlurbMismatch {
99        section: String,
100        structured: String,
101        prose: String,
102    },
103
104    // ===== lowering (this slice) — string → typed dispatch records =====
105    /// Neither `op_kind` nor `fused_op` is present, or both are present.
106    /// Exactly one must be declared (a primitive vs fused contract; §3.7,
107    /// V-FKC-5). The structural check happens at lower time.
108    #[error(
109        "FKC §3.7 (V-FKC-5): kernel `{section}` must declare exactly one of `op_kind` / \
110         `fused_op` (op_kind={op_kind:?}, fused_op={fused_op:?})"
111    )]
112    OpTargetAmbiguous {
113        section: String,
114        op_kind: Option<String>,
115        fused_op: Option<String>,
116    },
117
118    /// An `op_kind` string did not match any as-built `OpKind` variant
119    /// (§2.1; the lower-time `match` is exhaustive over `OpKind`).
120    #[error("FKC §2.1: kernel `{section}` names unknown op_kind `{op_kind}`")]
121    UnknownOpKind { section: String, op_kind: String },
122
123    /// A `fused_op` string did not match any registered `FusedOpId` name
124    /// (the name table is built from `fuel-graph`'s `default_registry()`;
125    /// §2.2).
126    #[error("FKC §2.2: kernel `{section}` names unknown fused_op `{fused_op}`")]
127    UnknownFusedOp { section: String, fused_op: String },
128
129    /// A dtype token (or a `dtype_class` shorthand expansion) did not
130    /// match any as-built `DType`/shorthand (§3.4). The explicit match
131    /// makes a bad token a typed error rather than a silent skip.
132    #[error("FKC §3.4: kernel `{section}` operand `{operand}` has bad dtype token `{token}`")]
133    BadScalarType {
134        section: String,
135        operand: String,
136        token: String,
137    },
138
139    /// A dtype token that KISS-CLASSIFY §6.1 **RESERVES** at this schema
140    /// version. It is a real, specified layout (the AMD `fnuz` FP8 forms) that
141    /// Fuel recognizes and deliberately does not compute on.
142    ///
143    /// # Why this is not [`FkcError::BadScalarType`]
144    ///
145    /// A reserved token and an unknown one imply different actions: reserved
146    /// means *wait for a schema bump*, unknown means *fix your contract*.
147    /// Reporting a correctly-spelled `F8E5M2FNUZ` as a "bad dtype token" sends
148    /// a contract author hunting for a typo that isn't there. FKC files are
149    /// external, hand-authored input, so this is a real ingest surface — which
150    /// is what makes the distinction worth a variant here.
151    ///
152    /// # What this is NOT
153    ///
154    /// It is not KISS-CLASSIFY §6.1-0001 conformance. That clause binds a
155    /// **reader of a `structure_key`**, and Fuel has none — it derives and
156    /// emits keys and treats foreign ones as opaque bytes (K1 opacity). This
157    /// is the reserved *vocabulary* of §6.1 applied to a surface Fuel actually
158    /// ingests.
159    ///
160    /// # This decline EXPIRES, on an external trigger (GAP-161)
161    ///
162    /// A decline for want of a kernel becomes wrong the moment the kernel
163    /// lands. This one becomes wrong when KISS activates the tokens — sk4
164    /// §6.1-0001 calls that "a future additive schema event". So it expires
165    /// too; what differs is that its trigger is **external and announced**,
166    /// and cannot quietly become true through a Fuel-side change. See
167    /// [`fuel_ir::RESERVED_DTYPE_TOKENS`], the single grep target to flip.
168    #[error(
169        "FKC §3.4 / KISS-CLASSIFY §6.1: kernel `{section}` operand `{operand}` names \
170         dtype token `{token}`, which is RESERVED at this schema version — recognized, \
171         deliberately not computed on. This is NOT an unknown token; do not look for a typo."
172    )]
173    ReservedScalarType {
174        section: String,
175        operand: String,
176        token: String,
177    },
178
179    /// Two operands that both **vary** (enumerate >1 dtype) in one section
180    /// disagree on their fan-out dtype list (§3.4 multi-dtype fan-out). ALL
181    /// varying operands must enumerate the SAME dtype list in the SAME order
182    /// so the importer can fan the section into one per-dtype binding without
183    /// silently picking one operand's list over another's.
184    #[error(
185        "FKC §3.4: kernel `{section}` operand `{operand}` varies over `{found}` but a prior \
186         varying operand enumerated `{expected}` — all varying operands must share one dtype \
187         list (and order) for multi-dtype fan-out"
188    )]
189    FanoutDtypeMismatch {
190        section: String,
191        operand: String,
192        expected: String,
193        found: String,
194    },
195
196    /// An `optional: true` operand is NOT the LAST input (§3.4 optional-operand
197    /// fan-out). Only the last input may be optional — omitting the last input
198    /// yields a CONTIGUOUS valid key ending just before the outputs, whereas an
199    /// earlier optional operand would leave a hole in the middle of the key and
200    /// mis-align every following operand. This is a typed error, never a silent
201    /// mis-key.
202    #[error(
203        "FKC §3.4: kernel `{section}` operand `{operand}` is `optional: true` but is not the \
204         last input — only the last input may be optional (omitting it yields a contiguous key)"
205    )]
206    OptionalOperandNotLast { section: String, operand: String },
207
208    /// A `passthrough(role)` output names the OPTIONAL operand as its dtype
209    /// source (§3.4 / §5.1). When the optional operand is absent the passthrough
210    /// cannot resolve, so its two keys (with / without the operand) would carry
211    /// DIFFERENT output dtypes — an output may not derive its dtype from an
212    /// operand that can be omitted. A typed error, never a silent mis-key.
213    #[error(
214        "FKC §3.4/§5.1: kernel `{section}` output `passthrough({role})` derives its dtype from \
215         the optional operand `{role}` — an output may not passthrough an optional operand"
216    )]
217    PassthroughNamesOptionalOperand { section: String, role: String },
218
219    /// A `backend` string did not match any as-built `BackendId` (§2.1).
220    #[error("FKC §2.1: kernel `{section}` names unknown backend `{backend}`")]
221    UnknownBackend { section: String, backend: String },
222
223    /// The `entry_point` symbol was absent from the provider link
224    /// registry (§12.6; P9 — the importer never fabricates a pointer).
225    #[error(
226        "FKC §12.6: kernel `{section}` entry_point `{entry_point}` is not in the link registry"
227    )]
228    UnknownEntryPoint {
229        section: String,
230        entry_point: String,
231    },
232
233    /// A `cost.cost_fn` NAME was absent from the provider link registry's
234    /// cost-fn table (§4.4 cost-fn trampoline; the cost-model analogue of
235    /// [`Self::UnknownEntryPoint`]). The importer never fabricates a cost-fn
236    /// pointer nor silently falls back to `unknown_cost` — a named-but-unknown
237    /// cost fn is a typed error.
238    #[error(
239        "FKC §4.4: kernel `{section}` cost_fn `{cost_fn}` is not in the link registry's cost-fn table"
240    )]
241    UnknownCostFn { section: String, cost_fn: String },
242
243    /// A `layout` tri-state flag carried a value outside the legal set
244    /// (`required` / `accepted` / `rejected` / `n/a`); §4.1, §6.
245    #[error(
246        "FKC §4.1: kernel `{section}` operand `{operand}` layout flag `{flag}` has illegal \
247         value `{value}` (expected required|accepted|rejected|n/a)"
248    )]
249    BadLayoutFlag {
250        section: String,
251        operand: String,
252        flag: String,
253        value: String,
254    },
255
256    /// A cost expression failed to parse against the §2.3 grammar
257    /// (V-FKC-8, §4.4). Carries the offending field + the raw expression.
258    #[error(
259        "FKC §4.4 (V-FKC-8): kernel `{section}` cost field `{field}` expression `{expr}` does \
260         not parse: {reason}"
261    )]
262    CostExprParse {
263        section: String,
264        field: String,
265        expr: String,
266        reason: String,
267    },
268
269    /// A precision block is a bare placeholder (no `audited`, no bounds,
270    /// no notes) — there is nothing to lower (§4.8, `PlaceholderPrecision`).
271    #[error(
272        "FKC §4.8: kernel `{section}` precision block is a bare placeholder (no audited flag, \
273         no bounds, no notes)"
274    )]
275    PlaceholderPrecision { section: String },
276
277    // ===== registration (this slice) — Resolved* → the two registries =====
278    /// A registration drove the same `KernelRef` function pointer onto one
279    /// `(op, dtypes, backend)` decision point twice (§3 / V-FKC-3). The
280    /// importer never panics on this; it surfaces the
281    /// [`crate::kernel::KernelBindingTable::finalize`] gate as a typed
282    /// error after all inserts. The string carries the dispatch-layer
283    /// message (which names the offending `(op, dtypes, backend)` key).
284    #[error("FKC §10 (V-FKC-3): duplicate KernelRef on a single decision point: {0}")]
285    DuplicateKernelRef(String),
286
287    /// `import_glob` collected files whose front-matter disagrees: the
288    /// `provider.name` / `provider.backend` / `provider.kernel_source`
289    /// must match across every file merged into one provider (§9.2).
290    #[error(
291        "FKC §9.2: provider front-matter mismatch merging globbed files — field `{field}` is \
292         `{found}` in `{file}` but the first file declared `{expected}`"
293    )]
294    ProviderMismatch {
295        field: String,
296        expected: String,
297        found: String,
298        file: String,
299    },
300
301    /// A glob / file-read I/O failure (no readable file, an unreadable
302    /// path, a bad glob pattern). Carries the offending path/pattern + the
303    /// underlying error text.
304    #[error("FKC: I/O error for `{path}`: {reason}")]
305    Io { path: String, reason: String },
306
307    // ===== §10 build-time validators (V-FKC-*) =====
308    /// `fkc_version` is not in the importer's declared supported set (V-FKC
309    /// §10.1; KISS #450 — membership, not a `<= max` bound, so `0` is rejected).
310    #[error("FKC §10.1: fkc_version {found} is not supported (supported versions: {supported:?})")]
311    UnsupportedVersion {
312        found: u32,
313        supported: &'static [u32],
314    },
315
316    /// Layout flags are mutually incoherent (V-FKC §10.4): neither
317    /// `contiguous` nor `strided` is acceptable; or `broadcast_stride0` /
318    /// `reverse_strides` accepted without `strided: accepted`.
319    #[error(
320        "FKC §10.4 (V-FKC-4): kernel `{section}` operand `{operand}` layout is incoherent: {reason}"
321    )]
322    LayoutIncoherent {
323        section: String,
324        operand: String,
325        reason: String,
326    },
327
328    /// A per-operand (or kernel-wide) `awkward_layout_strategy` contradicts
329    /// the operand's own layout flags, or carries an unknown value
330    /// (V-FKC §10.5; an unknown value is meaning-bearing per §11.1).
331    #[error(
332        "FKC §10.5 (V-FKC-5): kernel `{section}` operand `{operand}` awkward_layout_strategy \
333         `{strategy}` is incoherent: {reason}"
334    )]
335    AwkwardStrategyIncoherent {
336        section: String,
337        operand: String,
338        strategy: String,
339        reason: String,
340    },
341
342    /// A quant block is internally incoherent (V-FKC §10.6): a `GGML_BLOCK`
343    /// with a `ScalePair`/`granularity`, an `MX` without `granularity:
344    /// PerBlock`, an `AFFINE_*` with a bad granularity, an `AFFINE_BLOCK`
345    /// missing its block geometry, a `ggml_dtype` that is not a real
346    /// `GgmlDType` variant, a sub-byte operand with no `fdx.quant`, etc.
347    #[error(
348        "FKC §10.6 (V-FKC-6): kernel `{section}` operand `{operand}` quant is incoherent: {reason}"
349    )]
350    QuantIncoherent {
351        section: String,
352        operand: String,
353        reason: String,
354    },
355
356    /// A scale was declared in two places at once (V-FKC §10.6): both an
357    /// `fdx.quant.scale_operand` (separate-input scale) and a sidecar-bundled
358    /// scale for the same scale. Each scale lives in exactly one place.
359    #[error(
360        "FKC §10.6 (V-FKC-6): kernel `{section}` operand `{operand}` declares a scale in two \
361         places (scale_operand AND a sidecar scale): {reason}"
362    )]
363    ScaleDoubleDeclared {
364        section: String,
365        operand: String,
366        reason: String,
367    },
368
369    /// A contract is describable but NOT yet registrable on today's types
370    /// (V-FKC §10.6): `MX` (no `ScaleGranularity::PerBlock`) or `AFFINE_BLOCK`
371    /// (no block-quant descriptor target). The contract parse-validates; this
372    /// gates it at registration time (§6).
373    #[error(
374        "FKC §6/§10.6: kernel `{section}` quant family `{family}` parse-validates but is not yet \
375         registrable on today's types ({reason})"
376    )]
377    MxNotYetRegistrable {
378        section: String,
379        family: String,
380        reason: String,
381    },
382
383    /// An op-param `variant` is not a real variant **in the correct
384    /// namespace** (V-FKC §10.7): `OpParams` for an `op_kind` contract,
385    /// `FusedOpParams` for a `fused_op` contract (§3.7).
386    #[error(
387        "FKC §10.7 (V-FKC-7): kernel `{section}` op_params variant `{variant}` is not a real \
388         {namespace} variant"
389    )]
390    BadOpParamsVariant {
391        section: String,
392        variant: String,
393        namespace: String,
394    },
395
396    /// The required `cost.provenance` marker is absent or not one of
397    /// `{declared, judge_measured}` (V-FKC §10.8a; the COST_RULE).
398    #[error(
399        "FKC §10.8a: kernel `{section}` cost.provenance is missing or invalid (got {found:?}; \
400         expected `declared` or `judge_measured`)"
401    )]
402    CostProvenanceMissing {
403        section: String,
404        found: Option<String>,
405    },
406
407    /// A cost block is a bare / placeholder / zero-sentinel cost under either
408    /// provenance marker (V-FKC §10.8a) — not the honest `class: free`
409    /// metadata-only case.
410    #[error("FKC §10.8a: kernel `{section}` ships a placeholder/zero cost ({reason})")]
411    PlaceholderCost { section: String, reason: String },
412
413    /// An audited:false + all-null (UNAUDITED) precision on a non-reference
414    /// contract (V-FKC §10.9). (Reserved; the lint treats UNAUDITED as a
415    /// non-fatal note unless a stricter mode is requested.)
416    #[error("FKC §10.9 (V-FKC-9): kernel `{section}` ships UNAUDITED precision")]
417    UnauditedPrecision { section: String },
418
419    /// A bundle slot's shape rule yields rank > 6 (V-FKC §10.13), which the
420    /// serialized `FDXOutputView` (`[u64; 6]`) cannot represent.
421    #[error("FKC §10.13 (V-FKC-7): kernel `{section}` bundle slot `{slot}` has rank {rank} > 6")]
422    BundleSlotRankExceeded {
423        section: String,
424        slot: String,
425        rank: usize,
426    },
427
428    /// A `fused_op` contract's declared §5.1/§5.2 return rule disagrees with the
429    /// real registered `FusedOpEntry` fn at a probe shape (V-FKC-7, Finding 5.1).
430    /// `expected`/`actual` render either a shape or a dtype.
431    #[error(
432        "FKC §5 (V-FKC-7): kernel `{section}` output `{role}` declared return rule disagrees with \
433         the registered fused fn (declared {expected}, real {actual})"
434    )]
435    ShapeRuleMismatch {
436        section: String,
437        role: String,
438        expected: String,
439        actual: String,
440    },
441
442    /// A `return.bundle` slot count disagrees with the registered
443    /// `output_views` arity (V-FKC-7, Finding 5.2).
444    #[error(
445        "FKC §5.5 (V-FKC-7): kernel `{section}` declares {actual} bundle slots but output_views has {expected}"
446    )]
447    BundleArityMismatch {
448        section: String,
449        expected: usize,
450        actual: usize,
451    },
452
453    /// A `shape_constraint:` segment committed to §3.5 vocabulary but its
454    /// argument is malformed (`rank=banana`, an unclosed `divisible(`, an empty
455    /// `dim[0]=`). Non-vocabulary segments degrade to free text (a warning), not
456    /// this error — this fires only on a real authoring mistake in the grammar.
457    #[error(
458        "FKC §3.5: kernel `{section}` operand `{operand}` shape_constraint segment `{raw}` \
459         uses vocabulary but is malformed"
460    )]
461    UnparseableShapeConstraint {
462        section: String,
463        operand: String,
464        raw: String,
465    },
466
467    /// A paged/gather operand is incoherent (V-FKC §10.14): `kind:
468    /// paged_blocks` without `requires_ext: true` / `symbolic_extent:
469    /// required`, or `block_table`/`context_lens` naming a non-existent
470    /// `accept.inputs` role.
471    #[error(
472        "FKC §10.14 (V-FKC): kernel `{section}` operand `{operand}` gather is incoherent: {reason}"
473    )]
474    GatherIncoherent {
475        section: String,
476        operand: String,
477        reason: String,
478    },
479
480    /// A gather-bearing operand reached registration before the FDX gather
481    /// codes / `Capability::DlpackExtGather` landed (V-FKC §10.14; the
482    /// `MxNotYetRegistrable` discipline for gather).
483    #[error(
484        "FKC §3.9.1/§10.14: kernel `{section}` operand `{operand}` uses gather, not yet supported"
485    )]
486    GatherNotYetSupported { section: String, operand: String },
487
488    /// A dtype / quant family / granularity / ggml_dtype token is not a
489    /// member of FDX's normative code table (V-FKC §10.16 drift-guard) — so
490    /// FKC's token set stays a subset of FDX's.
491    #[error(
492        "FKC §10.16 (rule 16): kernel `{section}` token `{token}` (field `{field}`) is not in the \
493         FDX normative code table"
494    )]
495    FdxTokenNotInTable {
496        section: String,
497        field: String,
498        token: String,
499    },
500
501    /// An admissibility-affecting enum (`extent_kind`, `gather.kind`, quant
502    /// `family`) carried an unknown value (V-FKC §10.5/§10.14/§10.15;
503    /// meaning-bearing per §11.1 — a typed error, never a silent default).
504    #[error(
505        "FKC §11.1: kernel `{section}` field `{field}` has unknown meaning-bearing value `{value}`"
506    )]
507    UnknownAdmissibilityEnum {
508        section: String,
509        field: String,
510        value: String,
511    },
512
513    /// The structured `blurb:` does not match the prose blurb when the prose
514    /// is available (V-FKC §10.11). Distinct from [`Self::BlurbMismatch`]
515    /// (which is reserved for the prose-vs-structured comparison wired with
516    /// the prose-bearing parse).
517    #[error(
518        "FKC §10.11 (V-FKC-2): kernel `{section}` is missing a required blurb (must be a non-empty \
519         one-line string)"
520    )]
521    MissingBlurb { section: String },
522}
523
524impl FkcError {
525    /// Convenience: wrap a `serde_yaml_ng` error (optionally tagged with the
526    /// section name) into [`FkcError::Yaml`].
527    pub(crate) fn yaml(section: Option<&str>, err: impl std::fmt::Display) -> Self {
528        match section {
529            Some(s) => FkcError::Yaml(format!("section `{s}`: {err}")),
530            None => FkcError::Yaml(err.to_string()),
531        }
532    }
533}