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