Skip to main content

fuel_dispatch/fkc/
mod.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! # FKC kernel-contract importer (`fkc`)
3//!
4//! The Fuel Kernel Contract (FKC) importer: parse a provider's markdown
5//! contract file(s) and (eventually) auto-register every described kernel onto
6//! Fuel's dispatch surface (`KernelBindingTable` / `FusedKernelRegistry`) with
7//! zero hand-written registration glue.
8//!
9//! This module is unconditional production infrastructure (`pub mod fkc` in
10//! lib.rs, no feature gate; the `fkc` feature was removed in bd757464).
11//!
12//! ## Status — first slice
13//!
14//! Implemented here: the module skeleton, the [`FkcError`] type, the serde
15//! schema structs mirroring FKC §3.1 / §3.2 / §3.3, and the markdown +
16//! restricted-YAML parser ([`parse_file`] / [`parse_path`]) that:
17//! - splits the file-level `---` front-matter (§3.1),
18//! - extracts each `## ` section's single ` ```fkc ` block (§3.1),
19//! - enforces the §3.8 restricted YAML subset (tabs / anchors / aliases /
20//!   merge keys / the Norway problem) BEFORE deserializing, then
21//! - deserializes each block into the schema and assembles an [`FkcFile`].
22//!
23//! Also implemented: lowering to dispatch types (`lower.rs`), caps/cost/
24//! precision projection, the `LinkRegistry`, the revision hash, and — this
25//! slice — the END-TO-END registration path (`register.rs`):
26//! [`import_bundle_str`] / [`import_bundle`] / [`import_glob`] →
27//! [`ImportedProvider`] → [`ImportedProvider::register_into`] populating
28//! both the `KernelBindingTable` (primitives) and the
29//! `FusedKernelRegistry` (fused ops), with the duplicate-detection
30//! `finalize` gate surfaced as [`FkcError::DuplicateKernelRef`].
31//!
32//! NOT yet implemented (later slices of the plan): the cost trampoline /
33//! global cost-table (imported costs use the Judge-bootstrapped sentinel
34//! `CostFn` for now), the full `V-FKC-*` validators, and the CI lint.
35//!
36//! ## Authoritative references
37//!
38//! - Format spec: `docs/specs/kernel-contract-format.md`.
39//! - Adoption plan: `docs/session-prompts/kernel-contract-adoption-plan.md`.
40//! - Authored corpus (valid instances): `fuel-dispatch/kernel-contracts/**/*.fkc.md`.
41
42mod caps_map;
43pub(crate) mod cost_compile;
44pub(crate) mod cost_expr;
45mod cpu_link;
46#[cfg(feature = "cuda")]
47mod cuda_link;
48mod error;
49mod lower;
50mod parse;
51mod precision;
52mod register;
53mod return_check;
54mod revhash;
55mod schema;
56mod shape_constraint;
57// pub: the §6.20 shape-expression wire codec + evaluator is public interop
58// surface (byte-matches the KISS reference, golden-verified) — its decoder is
59// the reserved-tag decline point / future extension-registry activation point,
60// live API rather than crate-internal plumbing.
61pub mod shape_expr;
62pub(crate) mod shape_expr_parse;
63mod validate;
64// pub(crate): jit_ingest.rs (outside this module tree) reaches LedgerRecord
65// via crate::fkc::verify::LedgerRecord for RejectionReport::ledger_record.
66pub(crate) mod verify;
67// Contract-maintenance tooling for the `audited:` attestation (GAP-228).
68// OUTSIDE `verify/` on purpose: it writes `.fkc.md` files, and `verify/`'s
69// single-writer gate scans that directory for any write call. See its docs.
70// Tests exercise its guards; the `dev-tools` binary drives its two GAP-228 tools.
71#[cfg(any(test, feature = "dev-tools"))]
72pub(crate) mod contract_audit_flip;
73#[cfg(feature = "vulkan")]
74mod vulkan_link;
75mod warn;
76
77pub use caps_map::{ResolvedLayout, Tri, is_generic_contract};
78pub use cost_expr::{
79    CompiledCostExpr, CostEvalError, CostNode, bind_cost_symbols, cost_estimate, eval as eval_cost,
80};
81pub use cpu_link::{
82    CPU_AFFINE_CLAMP_POWI_ENTRY_POINTS, CPU_ATTENTION_ENTRY_POINTS, CPU_BINARY_ENTRY_POINTS,
83    CPU_COMPARE_ENTRY_POINTS, CPU_CONV_ENTRY_POINTS, CPU_FUSED_CONV_ROPE_ENTRY_POINTS,
84    CPU_FUSED_LINEAR_QUANT_ENTRY_POINTS, CPU_FUSED_NORM_ENTRY_POINTS, CPU_INPLACE_ENTRY_POINTS,
85    CPU_MATMUL_ENTRY_POINTS, CPU_NORM_BACKWARD_ENTRY_POINTS, CPU_NORM_ENTRY_POINTS,
86    CPU_PADDING_ENTRY_POINTS, CPU_REDUCE_ENTRY_POINTS, CPU_REDUCE_TO_ENTRY_POINTS,
87    CPU_ROPE_ENTRY_POINTS, CPU_SHAPE_OPS_ENTRY_POINTS, CPU_SSM_ENTRY_POINTS,
88    CPU_UNARY_ENTRY_POINTS, CPU_WHERE_ENTRY_POINTS, CpuLinkRegistry,
89};
90#[cfg(feature = "cuda")]
91pub use cuda_link::{
92    CUDA_AFFINE_ENTRY_POINTS, CUDA_ARG_REDUCE_ENTRY_POINTS, CUDA_BINARY_ENTRY_POINTS,
93    CUDA_CAST_ENTRY_POINTS, CUDA_CAUSAL_CONV1D_ENTRY_POINTS, CUDA_CLAMP_ENTRY_POINTS,
94    CUDA_CLAMP_INPLACE_ENTRY_POINTS, CUDA_CONCAT_ENTRY_POINTS, CUDA_COST_FNS,
95    CUDA_CUMSUM_ENTRY_POINTS, CUDA_FLASH_DECODING_ENTRY_POINTS, CUDA_FLIP_ENTRY_POINTS,
96    CUDA_GEMM_DENSE_ENTRY_POINTS, CUDA_GEMM_INT_ENTRY_POINTS, CUDA_INDEXING_ENTRY_POINTS,
97    CUDA_INPLACE_AFFINE_ENTRY_POINTS, CUDA_INPLACE_UNARY_ENTRY_POINTS, CUDA_NORM_ENTRY_POINTS,
98    CUDA_PAD_BACKWARD_ENTRY_POINTS, CUDA_PAD_ENTRY_POINTS, CUDA_POWI_BACKWARD_ENTRY_POINTS,
99    CUDA_POWI_ENTRY_POINTS, CUDA_POWI_INPLACE_ENTRY_POINTS, CUDA_REDUCE_ENTRY_POINTS,
100    CUDA_REDUCE_TO_ENTRY_POINTS, CUDA_ROLL_ENTRY_POINTS, CUDA_ROPE_ENTRY_POINTS,
101    CUDA_SOFTMAX_ENTRY_POINTS, CUDA_TRIANGULAR_ENTRY_POINTS, CUDA_UNARY_ENTRY_POINTS,
102    CUDA_WRITE_SLICE_ENTRY_POINTS, CUDA_WRITE_SLICE_ROTATING_ENTRY_POINTS, CudaLinkRegistry,
103};
104pub use error::FkcError;
105pub use lower::{LinkRegistry, Resolved, ResolvedFused, ResolvedPrimitive, lower_file};
106pub use parse::{parse_file, parse_path};
107pub use register::{
108    ImportedProvider, fused_unknown_cost, import_bundle, import_bundle_str, import_glob,
109};
110pub use revhash::compute_revision;
111pub use schema::{
112    AcceptBlock, CapsBlock, CostBlock, CostMemory, FdxSpec, FkcFile, FkcFrontMatter, FkcKernel,
113    FkcProvider, GatherSpec, LayoutSpec, OpParamFieldSpec, OpParamsSchema, OutputDesc,
114    PrecisionBlock, QuantSpec, ReturnBlock, TensorDesc,
115};
116pub use shape_constraint::{
117    AxisIndex, ProbeCombo, RankSpec, ShapeAtom, ShapeConstraint, parse_rank_spec,
118    parse_shape_constraint, solve_probe_shapes,
119};
120pub use validate::{FKC_SUPPORTED_VERSIONS, validate_file, validate_kernel};
121#[cfg(feature = "vulkan")]
122pub use vulkan_link::{
123    VULKAN_CAST_ENTRY_POINTS, VULKAN_ELEMENTWISE_ENTRY_POINTS, VulkanLinkRegistry,
124};
125pub use warn::ImportWarning;
126
127#[cfg(test)]
128mod tests {
129    use super::*;
130
131    // ----- Real-contract corpus paths (load-bearing schema-match test) -----
132
133    /// `elementwise-binary.fkc.md` — the simplest authored CPU contract.
134    const ELEMENTWISE_BINARY: &str =
135        include_str!("../../kernel-contracts/cpu/elementwise-binary.fkc.md");
136    /// `quant-matmul.fkc.md` — a more complex authored CPU contract (GGML quant
137    /// weight matmuls + NF4 with a separate scale operand, gemm cost class,
138    /// fdx.quant blocks).
139    const QUANT_MATMUL: &str = include_str!("../../kernel-contracts/cpu/quant-matmul.fkc.md");
140    /// `rope-apply.fkc.md` (Task 4.6) — baracuda's `rope_apply_<dt>_run`,
141    /// shipped but never wired into any dispatch path before this task. Its
142    /// `entry_point` names raw baracuda FFI symbols resolved only by a
143    /// harness-local `LinkRegistry` (`fuel-dispatch/src/fkc/verify/
144    /// harness.rs`, `#[cfg(feature = "cuda")]`), so it can only be
145    /// `import_bundle_str`'d for real under `--features cuda`. What CAN run
146    /// here, in the default build: parse + validate + lower against a STUB
147    /// registry (below) that resolves only the four EXACT expected
148    /// symbols — proof the contract is structurally well-formed (dtype-fan,
149    /// entry_point suffixing, per-operand fixed-vs-varying dtypes) without
150    /// touching any real CUDA code.
151    const ROPE_APPLY: &str = include_str!("../../kernel-contracts/cuda/rope-apply.fkc.md");
152
153    // =====================================================================
154    // PARSE A REAL CONTRACT — the key correctness test (§3.1/§3.3 schema match)
155    // =====================================================================
156
157    #[test]
158    fn parses_real_elementwise_binary_contract() {
159        let file =
160            parse_file(ELEMENTWISE_BINARY).expect("authored elementwise-binary.fkc.md must parse");
161
162        // Front-matter.
163        assert_eq!(file.front_matter.fkc_version, 1);
164        assert_eq!(file.front_matter.provider.name, "fuel-cpu-backend");
165        assert_eq!(file.front_matter.provider.backend, "Cpu");
166        assert_eq!(file.front_matter.provider.kernel_source, "portable-cpu");
167        assert_eq!(
168            file.front_matter.provider.link_registry.as_deref(),
169            Some("fuel_cpu_backend::fkc::ENTRY_POINTS")
170        );
171
172        // Non-empty kernels.
173        assert!(
174            !file.kernels.is_empty(),
175            "elementwise-binary must yield kernels"
176        );
177
178        // The umbrella `binary` chassis section + per-(op, dtype) thunks.
179        let names: Vec<&str> = file.kernels.iter().map(|k| k.kernel.as_str()).collect();
180        assert!(
181            names.contains(&"binary"),
182            "expected the `binary` chassis section; got {names:?}"
183        );
184        assert!(
185            names.contains(&"add_f32"),
186            "expected `add_f32`; got {names:?}"
187        );
188
189        // op_kind round-trips as a string; fused_op absent for these primitives.
190        let add_f32 = file
191            .kernels
192            .iter()
193            .find(|k| k.kernel == "add_f32")
194            .expect("add_f32 present");
195        assert_eq!(add_f32.op_kind.as_deref(), Some("AddElementwise"));
196        assert!(add_f32.fused_op.is_none());
197
198        // Accept block: two inputs (lhs, rhs), dtypes parsed as strings.
199        let accept = add_f32.accept.as_ref().expect("add_f32 has accept");
200        assert_eq!(accept.inputs.len(), 2);
201        assert_eq!(accept.inputs[0].name.as_deref(), Some("lhs"));
202        assert_eq!(accept.inputs[0].dtypes, vec!["F32".to_string()]);
203        // Layout five-flag set parsed.
204        let layout = accept.inputs[0].layout.as_ref().expect("lhs has layout");
205        assert_eq!(layout.contiguous.as_deref(), Some("required"));
206        assert_eq!(layout.strided.as_deref(), Some("rejected"));
207
208        // Return block: one output with rules carried as strings.
209        let ret = add_f32.return_.as_ref().expect("add_f32 has return");
210        assert_eq!(ret.outputs.len(), 1);
211        assert_eq!(
212            ret.outputs[0].dtype_rule.as_deref(),
213            Some("passthrough(lhs)")
214        );
215
216        // Cost: provenance + expression strings carried verbatim.
217        let cost = add_f32.cost.as_ref().expect("add_f32 has cost");
218        assert_eq!(cost.provenance.as_deref(), Some("judge_measured"));
219        assert_eq!(cost.flops.as_deref(), Some("n"));
220        assert_eq!(cost.bytes_moved.as_deref(), Some("3 * n * 4"));
221
222        // Determinism token.
223        assert_eq!(
224            add_f32.determinism.as_deref(),
225            Some("same_hardware_bitwise")
226        );
227    }
228
229    #[test]
230    fn parses_real_quant_matmul_contract() {
231        let file = parse_file(QUANT_MATMUL).expect("authored quant-matmul.fkc.md must parse");
232
233        assert_eq!(file.front_matter.provider.name, "fuel-cpu-backend");
234        assert!(!file.kernels.is_empty());
235
236        let names: Vec<&str> = file.kernels.iter().map(|k| k.kernel.as_str()).collect();
237        assert!(
238            names.contains(&"qmatmul_q4_0_f32"),
239            "expected qmatmul_q4_0_f32; got {names:?}"
240        );
241        assert!(
242            names.contains(&"nf4_matmul_f32"),
243            "expected nf4_matmul_f32; got {names:?}"
244        );
245
246        // A GGML quant weight operand: family/ggml_dtype parsed as strings,
247        // scale_operand stays None (INLINE single-place rule).
248        let q40 = file
249            .kernels
250            .iter()
251            .find(|k| k.kernel == "qmatmul_q4_0_f32")
252            .unwrap();
253        assert_eq!(q40.op_kind.as_deref(), Some("QMatMul"));
254        let weight = q40
255            .accept
256            .as_ref()
257            .unwrap()
258            .inputs
259            .iter()
260            .find(|d| d.name.as_deref() == Some("weight"))
261            .expect("q4_0 has a weight operand");
262        let quant = weight
263            .fdx
264            .as_ref()
265            .unwrap()
266            .quant
267            .as_ref()
268            .expect("weight has fdx.quant");
269        assert_eq!(quant.family.as_deref(), Some("GGML_BLOCK"));
270        assert_eq!(quant.ggml_dtype.as_deref(), Some("Q4_0"));
271        assert_eq!(quant.role.as_deref(), Some("weight"));
272        assert!(
273            quant.scale_operand.is_none(),
274            "INLINE scale: no separate operand"
275        );
276
277        // op_params variant + a field with a constraint string.
278        let op_params = q40.accept.as_ref().unwrap().op_params.as_ref().unwrap();
279        assert_eq!(op_params.variant.as_deref(), Some("QMatMul"));
280        assert!(op_params.fields.contains_key("k"));
281
282        // NF4: the absmax is a SEPARATE scale operand (single-place rule).
283        let nf4 = file
284            .kernels
285            .iter()
286            .find(|k| k.kernel == "nf4_matmul_f32")
287            .unwrap();
288        let nf4_inputs = &nf4.accept.as_ref().unwrap().inputs;
289        assert!(
290            nf4_inputs
291                .iter()
292                .any(|d| d.name.as_deref() == Some("absmax")),
293            "NF4 has a separate absmax operand"
294        );
295        let w_packed = nf4_inputs
296            .iter()
297            .find(|d| d.name.as_deref() == Some("w_packed"))
298            .unwrap();
299        let nf4_quant = w_packed.fdx.as_ref().unwrap().quant.as_ref().unwrap();
300        assert_eq!(nf4_quant.family.as_deref(), Some("AFFINE_BLOCK"));
301        assert_eq!(nf4_quant.scale_operand.as_deref(), Some("absmax"));
302    }
303
304    /// Task 4.6 — the rope-apply CUDA acceptance-kernel contract must
305    /// parse, pass the `V-FKC-*` validators, and LOWER structurally
306    /// correctly, all WITHOUT the `cuda` feature (a stub registry stands
307    /// in for the real link, matching `lower.rs`'s own `StubLink`
308    /// precedent). This is the default-feature verification the Task 4.6
309    /// brief explicitly permits in lieu of a real `--features cuda` import.
310    #[test]
311    fn parses_and_lowers_real_rope_apply_contract() {
312        use fuel_ir::DType;
313        use fuel_ir::dispatch::OpKind;
314        use fuel_ir::probe::BackendId;
315
316        let file = parse_file(ROPE_APPLY).expect("rope-apply.fkc.md must parse");
317        validate_file(&file).expect("rope-apply.fkc.md must pass the V-FKC-* validators");
318
319        assert_eq!(file.front_matter.provider.name, "fuel-cuda-backend");
320        assert_eq!(file.front_matter.provider.backend, "Cuda");
321        assert_eq!(file.front_matter.provider.kernel_source, "baracuda");
322
323        let names: Vec<&str> = file.kernels.iter().map(|k| k.kernel.as_str()).collect();
324        assert!(
325            names.contains(&"rope_apply"),
326            "expected a `rope_apply` section; got {names:?}"
327        );
328
329        let kernel = file
330            .kernels
331            .iter()
332            .find(|k| k.kernel == "rope_apply")
333            .unwrap();
334        assert_eq!(kernel.op_kind.as_deref(), Some("Rope"));
335        assert!(
336            kernel.fused_op.is_none(),
337            "rope_apply is a primitive op_kind contract, not fused"
338        );
339
340        // Three inputs: x (fans over the 4 dtypes), cos + sin (fixed F32 each,
341        // per baracuda's ABI — see the contract's front-matter note).
342        let accept = kernel.accept.as_ref().expect("rope_apply has accept");
343        assert_eq!(accept.inputs.len(), 3);
344        assert_eq!(accept.inputs[0].name.as_deref(), Some("x"));
345        assert_eq!(
346            accept.inputs[0].dtypes,
347            vec![
348                "F32".to_string(),
349                "F16".to_string(),
350                "BF16".to_string(),
351                "F64".to_string()
352            ],
353        );
354        assert_eq!(accept.inputs[1].name.as_deref(), Some("cos"));
355        assert_eq!(accept.inputs[1].dtypes, vec!["F32".to_string()]);
356        assert_eq!(accept.inputs[2].name.as_deref(), Some("sin"));
357        assert_eq!(accept.inputs[2].dtypes, vec!["F32".to_string()]);
358
359        // Lower against a stub that resolves ONLY the four raw baracuda symbols
360        // this contract's base entry_point ("baracuda_kernels_rope_apply") fans
361        // into — a wrong suffix, or a missing/extra variant, fails this.
362        fn dummy_kernel(
363            _inputs: &[std::sync::Arc<std::sync::RwLock<fuel_memory::Storage>>],
364            _outputs: &mut [std::sync::Arc<std::sync::RwLock<fuel_memory::Storage>>],
365            _layouts: &[fuel_ir::Layout],
366            _params: &crate::kernel::OpParams,
367        ) -> fuel_ir::Result<()> {
368            Ok(())
369        }
370        struct StubRopeApplyLink;
371        impl LinkRegistry for StubRopeApplyLink {
372            fn resolve_primitive(&self, symbol: &str) -> Option<crate::kernel::KernelRef> {
373                const KNOWN: &[&str] = &[
374                    "baracuda_kernels_rope_apply_f32",
375                    "baracuda_kernels_rope_apply_f16",
376                    "baracuda_kernels_rope_apply_bf16",
377                    "baracuda_kernels_rope_apply_f64",
378                ];
379                if KNOWN.contains(&symbol) {
380                    Some(dummy_kernel)
381                } else {
382                    None
383                }
384            }
385            fn resolve_fused(&self, _symbol: &str) -> Option<crate::kernel::KernelRef> {
386                None
387            }
388        }
389
390        let mut warnings = Vec::new();
391        let resolved = lower_file(&file, &StubRopeApplyLink, &mut warnings)
392            .expect("rope-apply.fkc.md must lower against a registry resolving its 4 real symbols");
393        let primitives: Vec<_> = resolved
394            .iter()
395            .filter_map(|r| match r {
396                Resolved::Primitive(p) => Some(p),
397                Resolved::Fused(_) => None,
398            })
399            .collect();
400        assert_eq!(
401            primitives.len(),
402            4,
403            "expected 4 dtype variants (F32/F16/BF16/F64); got {}",
404            primitives.len(),
405        );
406        for p in &primitives {
407            assert_eq!(p.op, OpKind::Rope);
408            assert_eq!(p.backend, BackendId::Cuda);
409            // Per-variant key: [x_dt, F32(cos), F32(sin), x_dt(out, passthrough)].
410            assert_eq!(
411                p.dtypes.len(),
412                4,
413                "expected [x, cos, sin, out] key; got {:?}",
414                p.dtypes
415            );
416            assert_eq!(p.dtypes[1], DType::F32, "cos is always F32");
417            assert_eq!(p.dtypes[2], DType::F32, "sin is always F32");
418            assert_eq!(
419                p.dtypes[0], p.dtypes[3],
420                "output passthrough(x) must match x's dtype"
421            );
422        }
423        let x_dtypes: std::collections::HashSet<DType> =
424            primitives.iter().map(|p| p.dtypes[0]).collect();
425        assert_eq!(
426            x_dtypes,
427            [DType::F32, DType::F16, DType::BF16, DType::F64]
428                .into_iter()
429                .collect(),
430            "expected exactly the F32/F16/BF16/F64 fan",
431        );
432    }
433
434    // =====================================================================
435    // §3.8 NEGATIVES
436    // =====================================================================
437
438    const VALID_MINIMAL: &str = "\
439---
440fkc_version: 1
441provider:
442  name: test-provider
443  backend: Cpu
444  kernel_source: \"test-cpu\"
445---
446
447# test bundle
448
449## demo
450
451A blurb.
452
453```fkc
454kernel: demo
455op_kind: AddElementwise
456blurb: \"demo\"
457entry_point: \"x::y\"
458```
459";
460
461    #[test]
462    fn valid_minimal_parses() {
463        let file = parse_file(VALID_MINIMAL).expect("minimal valid file parses");
464        assert_eq!(file.kernels.len(), 1);
465        assert_eq!(file.kernels[0].kernel, "demo");
466    }
467
468    #[test]
469    fn tab_indentation_in_block_is_rejected() {
470        // A tab-indented line inside the fkc block.
471        let src = "\
472---
473fkc_version: 1
474provider:
475  name: p
476  backend: Cpu
477  kernel_source: \"c\"
478---
479
480## demo
481
482b
483
484```fkc
485kernel: demo
486accept:
487\tinputs: []
488```
489";
490        let err = parse_file(src).expect_err("tab must be rejected");
491        assert!(
492            matches!(err, FkcError::TabIndentation { .. }),
493            "got {err:?}"
494        );
495    }
496
497    #[test]
498    fn anchor_is_rejected() {
499        let src = "\
500---
501fkc_version: 1
502provider:
503  name: p
504  backend: Cpu
505  kernel_source: \"c\"
506---
507
508## demo
509
510b
511
512```fkc
513kernel: &base demo
514op_kind: AddElementwise
515```
516";
517        let err = parse_file(src).expect_err("anchor must be rejected");
518        assert!(
519            matches!(err, FkcError::AnchorDisallowed { .. }),
520            "got {err:?}"
521        );
522    }
523
524    #[test]
525    fn alias_is_rejected() {
526        let src = "\
527---
528fkc_version: 1
529provider:
530  name: p
531  backend: Cpu
532  kernel_source: \"c\"
533---
534
535## demo
536
537b
538
539```fkc
540kernel: demo
541blurb: *base
542```
543";
544        let err = parse_file(src).expect_err("alias must be rejected");
545        assert!(
546            matches!(err, FkcError::AliasDisallowed { .. }),
547            "got {err:?}"
548        );
549    }
550
551    #[test]
552    fn merge_key_is_rejected() {
553        let src = "\
554---
555fkc_version: 1
556provider:
557  name: p
558  backend: Cpu
559  kernel_source: \"c\"
560---
561
562## demo
563
564b
565
566```fkc
567kernel: demo
568caps:
569  <<: defaults
570```
571";
572        let err = parse_file(src).expect_err("merge key must be rejected");
573        assert!(
574            matches!(err, FkcError::MergeKeyDisallowed { .. }),
575            "got {err:?}"
576        );
577    }
578
579    #[test]
580    fn unquoted_norway_token_is_rejected() {
581        // `family: no` unquoted, in a value position — must be flagged, never
582        // silently coerced to a bool.
583        let src = "\
584---
585fkc_version: 1
586provider:
587  name: p
588  backend: Cpu
589  kernel_source: \"c\"
590---
591
592## demo
593
594b
595
596```fkc
597kernel: demo
598family: no
599```
600";
601        let err = parse_file(src).expect_err("unquoted Norway token must be rejected");
602        assert!(
603            matches!(err, FkcError::NorwayToken { ref token, .. } if token == "no"),
604            "got {err:?}"
605        );
606    }
607
608    #[test]
609    fn quoted_norway_token_is_accepted_as_string() {
610        // `family: "none"` (quoted) must NOT trip the Norway check and must
611        // deserialize to the string "none".
612        let src = "\
613---
614fkc_version: 1
615provider:
616  name: p
617  backend: Cpu
618  kernel_source: \"c\"
619---
620
621## demo
622
623b
624
625```fkc
626kernel: demo
627accept:
628  inputs:
629    - name: lhs
630      dtypes: [F32]
631      fdx:
632        quant:
633          family: \"none\"
634```
635";
636        let file = parse_file(src).expect("quoted token parses");
637        let fam = file.kernels[0].accept.as_ref().unwrap().inputs[0]
638            .fdx
639            .as_ref()
640            .unwrap()
641            .quant
642            .as_ref()
643            .unwrap()
644            .family
645            .as_deref();
646        assert_eq!(fam, Some("none"));
647    }
648
649    // =====================================================================
650    // §3.1 fenced-block anatomy
651    // =====================================================================
652
653    #[test]
654    fn missing_fkc_block_is_rejected() {
655        let src = "\
656---
657fkc_version: 1
658provider:
659  name: p
660  backend: Cpu
661  kernel_source: \"c\"
662---
663
664## demo
665
666Just prose, no fkc block.
667";
668        let err = parse_file(src).expect_err("section with no fkc block must error");
669        assert!(
670            matches!(err, FkcError::MissingFkcBlock { ref section } if section == "demo"),
671            "got {err:?}"
672        );
673    }
674
675    #[test]
676    fn duplicate_fkc_block_is_rejected() {
677        let src = "\
678---
679fkc_version: 1
680provider:
681  name: p
682  backend: Cpu
683  kernel_source: \"c\"
684---
685
686## demo
687
688b
689
690```fkc
691kernel: demo
692```
693
694more prose
695
696```fkc
697kernel: demo2
698```
699";
700        let err = parse_file(src).expect_err("section with two fkc blocks must error");
701        assert!(
702            matches!(err, FkcError::MultipleFkcBlocks { ref section, count } if section == "demo" && count == 2),
703            "got {err:?}"
704        );
705    }
706
707    #[test]
708    fn missing_front_matter_is_rejected() {
709        let src = "## demo\n\n```fkc\nkernel: demo\n```\n";
710        let err = parse_file(src).expect_err("file without front-matter must error");
711        assert!(
712            matches!(err, FkcError::MalformedFrontMatter(_)),
713            "got {err:?}"
714        );
715    }
716
717    // =====================================================================
718    // §3.1 orphan ```fkc block (outside any `## ` section) — a silent no-op
719    // import bug: `split_sections` drops everything before the first `## `
720    // heading, so a block placed there vanished with `Ok(FkcFile { kernels:
721    // [] })` instead of a typed error.
722    // =====================================================================
723
724    #[test]
725    fn orphan_fkc_block_outside_a_section_is_rejected() {
726        // A fkc block under only an H1, no `## ` heading — today silently dropped.
727        let src = "---\nfkc_version: 1\nprovider:\n  name: p\n  backend: Cpu\n  kernel_source: \"ks\"\n---\n\n# Title\n\n```fkc\nkernel: add_f32\nop_kind: AddElementwise\n```\n";
728        let err = parse_file(src).expect_err(
729            "an fkc block outside any `## ` section must be rejected, not silently dropped",
730        );
731        assert!(
732            matches!(err, FkcError::OrphanFkcBlock { .. }),
733            "got {err:?}"
734        );
735    }
736
737    #[test]
738    fn fkc_block_with_no_sections_at_all_is_rejected() {
739        let src = "---\nfkc_version: 1\nprovider:\n  name: p\n  backend: Cpu\n  kernel_source: \"ks\"\n---\n\n```fkc\nkernel: add_f32\nop_kind: AddElementwise\n```\n";
740        assert!(matches!(
741            parse_file(src).unwrap_err(),
742            FkcError::OrphanFkcBlock { .. }
743        ));
744    }
745
746    #[test]
747    fn non_fkc_fence_before_first_heading_is_not_flagged() {
748        // A yaml doc example in the intro prose must NOT trigger the orphan check.
749        let src = "---\nfkc_version: 1\nprovider:\n  name: p\n  backend: Cpu\n  kernel_source: \"ks\"\n---\n\n# Title\n\n```yaml\nexample: true\n```\n\n## add_f32\n\n```fkc\nkernel: add_f32\nop_kind: AddElementwise\n```\n";
750        parse_file(src).expect("a non-fkc intro fence + a real section parses fine");
751    }
752
753    #[test]
754    fn indented_pseudo_heading_does_not_hide_a_following_orphan_fkc_block() {
755        // An indented `  ## foo` is NOT a real heading to split_sections (column-0
756        // only), so a ```fkc block after it is still an orphan and must be rejected —
757        // the detector must not treat the indented line as "a section started".
758        let src = "---\nfkc_version: 1\nprovider:\n  name: p\n  backend: Cpu\n  kernel_source: \"ks\"\n---\n\n# Title\n\n  ## not-a-real-heading (indented)\n\n```fkc\nkernel: add_f32\nop_kind: AddElementwise\n```\n";
759        let err = parse_file(src)
760            .expect_err("an indented pseudo-heading must not hide a following orphan fkc block");
761        assert!(
762            matches!(err, FkcError::OrphanFkcBlock { .. }),
763            "got {err:?}"
764        );
765    }
766}