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}