1use crate::fkc::error::FkcError;
44use crate::fkc::lower;
45use crate::fkc::schema::{
46 AcceptBlock, CapsBlock, CostBlock, CostMemory, FdxSpec, FkcFile, FkcFrontMatter, FkcKernel,
47 FkcProvider, GatherSpec, LayoutSpec, OpParamFieldSpec, OpParamsSchema, OutputDesc,
48 PrecisionBlock, QuantSpec, ReturnBlock, TensorDesc,
49};
50use crate::kernel::OpParams;
53use fuel_graph::registry::FusedOpParams;
54
55pub const FKC_SUPPORTED_VERSIONS: &[u32] = &[1];
87
88pub(crate) fn fkc_version_is_supported(version: u32) -> bool {
93 FKC_SUPPORTED_VERSIONS.contains(&version)
94}
95
96fn _fkc_pin_front_matter(front_matter: FkcFrontMatter, provider: FkcProvider) {
126 let FkcFrontMatter {
127 fkc_version: _,
128 provider: _,
129 } = front_matter;
130 let FkcProvider {
131 name: _,
132 backend: _,
133 kernel_source: _,
134 link_registry: _,
135 revision_base: _,
136 } = provider;
137}
138
139fn _fkc_pin_kernel(kernel: FkcKernel) {
140 let FkcKernel {
141 kernel: _,
142 registrable: _,
143 op_kind: _,
144 fused_op: _,
145 blurb: _,
146 backend: _,
147 kernel_source: _,
148 entry_point: _,
149 kernel_revision_hash: _,
150 variant: _,
151 accept: _,
152 return_: _,
153 caps: _,
154 cost: _,
155 precision: _,
156 determinism: _,
157 } = kernel;
158}
159
160fn _fkc_pin_accept(
161 accept: AcceptBlock,
162 op_params: OpParamsSchema,
163 op_param_field: OpParamFieldSpec,
164) {
165 let AcceptBlock {
166 inputs: _,
167 op_params: _,
168 } = accept;
169 let OpParamsSchema {
170 variant: _,
171 fields: _,
172 } = op_params;
173 let OpParamFieldSpec {
174 kind: _,
175 constraint: _,
176 note: _,
177 } = op_param_field;
178}
179
180fn _fkc_pin_return(ret: ReturnBlock, output: OutputDesc) {
181 let ReturnBlock {
182 outputs: _,
183 bundle: _,
184 } = ret;
185 let OutputDesc {
186 name: _,
187 dtype_rule: _,
188 shape_rule: _,
189 layout_guarantee: _,
190 aliasing: _,
191 } = output;
192}
193
194fn _fkc_pin_tensor(
195 tensor: TensorDesc,
196 layout: LayoutSpec,
197 fdx: FdxSpec,
198 quant: QuantSpec,
199 gather: GatherSpec,
200) {
201 let TensorDesc {
202 name: _,
203 optional: _,
204 dtypes: _,
205 dtype_class: _,
206 layout: _,
207 rank: _,
208 shape_constraint: _,
209 fdx: _,
210 device: _,
211 substrate: _,
212 } = tensor;
213 let LayoutSpec {
214 contiguous: _,
215 strided: _,
216 broadcast_stride0: _,
217 broadcast_axes: _,
218 start_offset: _,
219 reverse_strides: _,
220 awkward_layout_strategy: _,
221 } = layout;
222 let FdxSpec {
223 requires_ext: _,
224 quant: _,
225 sub_byte: _,
226 symbolic_extent: _,
227 extent_kind: _,
228 gather: _,
229 } = fdx;
230 let QuantSpec {
231 family: _,
232 ggml_dtype: _,
233 granularity: _,
234 block_shape: _,
235 role: _,
236 scale_operand: _,
237 } = quant;
238 let GatherSpec {
239 kind: _,
240 block_table: _,
241 context_lens: _,
242 } = gather;
243}
244
245fn _fkc_pin_caps_cost_precision(
246 caps: CapsBlock,
247 cost: CostBlock,
248 cost_memory: CostMemory,
249 precision: PrecisionBlock,
250) {
251 let CapsBlock {
252 awkward_layout_strategy: _,
253 fast_paths: _,
254 in_place: _,
255 alignment_bytes: _,
256 access_granularity_bits: _,
257 } = caps;
258 let CostBlock {
259 provenance: _,
260 class: _,
261 cost_fn: _,
262 flops: _,
263 bytes_moved: _,
264 overhead_ns: _,
265 memory: _,
266 } = cost;
267 let CostMemory {
268 device_bytes: _,
269 host_bytes: _,
270 disk_bytes: _,
271 } = cost_memory;
272 let PrecisionBlock {
273 bit_stable_on_same_hardware: _,
274 max_ulp: _,
275 max_relative: _,
276 max_absolute: _,
277 audited: _,
278 notes: _,
279 } = precision;
280}
281
282fn is_fdx_quant_family(tok: &str) -> bool {
297 matches!(
298 tok,
299 "none" | "NONE" | "GGML_BLOCK" | "MX" | "AFFINE_INT" | "AFFINE_FLOAT" | "AFFINE_BLOCK"
300 )
301}
302
303fn is_fdx_granularity(tok: &str) -> bool {
306 matches!(tok, "PerTensor" | "PerToken" | "PerChannel" | "PerBlock")
307}
308
309fn ggml_dtype_code(tok: &str) -> Option<u32> {
314 let code = match tok {
315 "F32" => 0,
316 "F16" => 1,
317 "Q4_0" => 2,
318 "Q4_1" => 3,
319 "Q5_0" => 6,
320 "Q5_1" => 7,
321 "Q8_0" => 8,
322 "Q8_1" => 9,
323 "Q2K" => 10,
324 "Q3K" => 11,
325 "Q4K" => 12,
326 "Q5K" => 13,
327 "Q6K" => 14,
328 "Q8K" => 15,
329 "BF16" => 30,
330 _ => return None,
331 };
332 Some(code)
333}
334
335fn is_registrable_granularity(tok: &str) -> bool {
339 matches!(tok, "PerTensor" | "PerToken" | "PerChannel")
340}
341
342pub fn validate_file(file: &FkcFile) -> Result<(), FkcError> {
350 if !fkc_version_is_supported(file.front_matter.fkc_version) {
352 return Err(FkcError::UnsupportedVersion {
353 found: file.front_matter.fkc_version,
354 supported: FKC_SUPPORTED_VERSIONS,
355 });
356 }
357 for kernel in &file.kernels {
358 match validate_kernel(kernel) {
359 Ok(()) => {}
360 Err(FkcError::GatherNotYetSupported { .. } | FkcError::MxNotYetRegistrable { .. })
374 if !kernel.registrable => {}
375 Err(e) => return Err(e),
376 }
377 }
378 Ok(())
379}
380
381pub fn validate_kernel(kernel: &FkcKernel) -> Result<(), FkcError> {
385 let section = kernel.kernel.as_str();
386
387 let describe_only = !kernel.registrable;
391
392 required_fields(kernel, section, describe_only)?;
394
395 let is_fused = if describe_only {
399 false
402 } else {
403 match (kernel.op_kind.as_deref(), kernel.fused_op.as_deref()) {
404 (Some(op), None) => {
405 lower::lower_op_kind(op, section)?;
407 false
408 }
409 (None, Some(fused)) => {
410 lower::lower_fused_op(fused, section)?;
412 true
413 }
414 (op, fused) => {
415 return Err(FkcError::OpTargetAmbiguous {
416 section: section.to_string(),
417 op_kind: op.map(String::from),
418 fused_op: fused.map(String::from),
419 });
420 }
421 }
422 };
423
424 if let Some(accept) = &kernel.accept {
427 for d in &accept.inputs {
428 let operand = d.name.as_deref().unwrap_or("<input>").to_string();
429 validate_operand(section, &operand, d, accept)?;
430 }
431 if !describe_only
434 && let Some(op_params) = &accept.op_params
435 && let Some(variant) = op_params.variant.as_deref()
436 {
437 validate_op_params_namespace(section, variant, is_fused)?;
438 }
439 }
440
441 if let Some(ret) = &kernel.return_ {
444 for out in &ret.outputs {
445 if let Some(rule) = out.dtype_rule.as_deref() {
448 check_output_dtype_rule(section, out.name.as_deref().unwrap_or("<output>"), rule)?;
449 }
450 }
451 if let Some(bundle) = &ret.bundle {
453 check_bundle_ranks(section, bundle)?;
454 }
455 }
456
457 validate_cost(kernel, section)?;
459
460 validate_precision_coherence(kernel, section)?;
462
463 Ok(())
464}
465
466fn required_fields(kernel: &FkcKernel, section: &str, describe_only: bool) -> Result<(), FkcError> {
471 match kernel.blurb.as_deref() {
476 Some(b) if !b.trim().is_empty() => {}
477 _ => {
478 return Err(FkcError::MissingBlurb {
479 section: section.to_string(),
480 });
481 }
482 }
483
484 require(kernel.entry_point.as_deref(), section, "entry_point")?;
486
487 if !describe_only {
494 let has_inputs = kernel
495 .accept
496 .as_ref()
497 .map(|a| !a.inputs.is_empty())
498 .unwrap_or(false);
499 if !has_inputs {
500 return Err(FkcError::MissingRequiredField {
501 section: section.to_string(),
502 field: "accept.inputs (≥1)".to_string(),
503 });
504 }
505 }
506
507 let has_outputs = kernel
509 .return_
510 .as_ref()
511 .map(|r| !r.outputs.is_empty() || r.bundle.is_some())
512 .unwrap_or(false);
513 if !has_outputs {
514 return Err(FkcError::MissingRequiredField {
515 section: section.to_string(),
516 field: "return.outputs (≥1) or return.bundle".to_string(),
517 });
518 }
519
520 if kernel.cost.is_none() {
522 return Err(FkcError::MissingRequiredField {
523 section: section.to_string(),
524 field: "cost".to_string(),
525 });
526 }
527 if kernel.precision.is_none() {
529 return Err(FkcError::MissingRequiredField {
530 section: section.to_string(),
531 field: "precision".to_string(),
532 });
533 }
534 require(kernel.determinism.as_deref(), section, "determinism")?;
536
537 Ok(())
538}
539
540fn require(v: Option<&str>, section: &str, field: &str) -> Result<(), FkcError> {
541 match v {
542 Some(s) if !s.trim().is_empty() => Ok(()),
543 _ => Err(FkcError::MissingRequiredField {
544 section: section.to_string(),
545 field: field.to_string(),
546 }),
547 }
548}
549
550fn validate_operand(
555 section: &str,
556 operand: &str,
557 d: &TensorDesc,
558 accept: &crate::fkc::schema::AcceptBlock,
559) -> Result<(), FkcError> {
560 for tok in &d.dtypes {
562 lower::lower_dtype(tok, section, operand).map_err(|_| FkcError::FdxTokenNotInTable {
563 section: section.to_string(),
564 field: format!("{operand}.dtypes"),
565 token: tok.clone(),
566 })?;
567 }
568
569 if let Some(layout) = &d.layout {
571 layout_coherence(section, operand, layout)?;
572 }
573
574 awkward_strategy_coherence(section, operand, d, accept)?;
576
577 if let Some(fdx) = &d.fdx {
580 let is_sub_byte = d
582 .dtypes
583 .iter()
584 .any(|t| matches!(t.as_str(), "F4" | "F6E2M3" | "F6E3M2"));
585 if is_sub_byte && fdx.quant.is_none() && fdx.sub_byte.is_none() {
586 return Err(FkcError::QuantIncoherent {
587 section: section.to_string(),
588 operand: operand.to_string(),
589 reason: "sub-byte dtype requires an fdx.sub_byte + fdx.quant block (no reliance \
590 on size_in_bytes()==0)"
591 .to_string(),
592 });
593 }
594
595 if let Some(quant) = &fdx.quant {
596 quant_coherence(section, operand, quant, accept)?;
597 }
598
599 if let Some(gather) = &fdx.gather {
601 gather_coherence(section, operand, gather, fdx, accept)?;
602 }
603
604 extent_coherence(section, operand, fdx)?;
606 }
607
608 Ok(())
609}
610
611fn layout_coherence(
614 section: &str,
615 operand: &str,
616 layout: &crate::fkc::schema::LayoutSpec,
617) -> Result<(), FkcError> {
618 use crate::fkc::caps_map::{Tri, resolve_layout};
619 let r = resolve_layout(Some(layout), section, operand)?;
622
623 let contiguous_ok = matches!(r.contiguous, Tri::Required | Tri::Accepted);
624 let strided_ok = matches!(r.strided, Tri::Accepted);
625
626 if !contiguous_ok && !strided_ok {
628 return Err(FkcError::LayoutIncoherent {
629 section: section.to_string(),
630 operand: operand.to_string(),
631 reason: "neither `contiguous` (required|accepted) nor `strided: accepted` is set — \
632 the kernel accepts no layout"
633 .to_string(),
634 });
635 }
636
637 if matches!(r.broadcast_stride0, Tri::Accepted) && !strided_ok {
639 return Err(FkcError::LayoutIncoherent {
640 section: section.to_string(),
641 operand: operand.to_string(),
642 reason: "`broadcast_stride0: accepted` requires `strided: accepted` (broadcast is a \
643 stride-0 special case)"
644 .to_string(),
645 });
646 }
647 if matches!(r.reverse_strides, Tri::Accepted) && !strided_ok {
649 return Err(FkcError::LayoutIncoherent {
650 section: section.to_string(),
651 operand: operand.to_string(),
652 reason: "`reverse_strides: accepted` requires `strided: accepted` (a negative stride \
653 is still a strided walk)"
654 .to_string(),
655 });
656 }
657
658 match (
665 &layout.broadcast_axes,
666 matches!(r.broadcast_stride0, Tri::Required),
667 ) {
668 (Some(axes), true) if axes.is_empty() => {
669 return Err(FkcError::LayoutIncoherent {
670 section: section.to_string(),
671 operand: operand.to_string(),
672 reason: "`broadcast_stride0: required` with an EMPTY `broadcast_axes` — name the \
673 iteration axes the operand must be stride-0 on"
674 .to_string(),
675 });
676 }
677 (Some(_), false) => {
678 return Err(FkcError::LayoutIncoherent {
679 section: section.to_string(),
680 operand: operand.to_string(),
681 reason: "`broadcast_axes` set without `broadcast_stride0: required` — the mask is \
682 meaningful only for a required (baked) broadcast"
683 .to_string(),
684 });
685 }
686 (None, true) => {
687 return Err(FkcError::LayoutIncoherent {
688 section: section.to_string(),
689 operand: operand.to_string(),
690 reason: "`broadcast_stride0: required` needs `broadcast_axes` — the shape-blind \
691 binder can't verify the broadcast without the axis mask"
692 .to_string(),
693 });
694 }
695 _ => {}
696 }
697 Ok(())
698}
699
700fn awkward_strategy_coherence(
703 section: &str,
704 operand: &str,
705 d: &TensorDesc,
706 _accept: &crate::fkc::schema::AcceptBlock,
707) -> Result<(), FkcError> {
708 use crate::fkc::caps_map::{Tri, resolve_layout};
709
710 let per_operand = d
715 .layout
716 .as_ref()
717 .and_then(|l| l.awkward_layout_strategy.as_deref());
718
719 let Some(strategy) = per_operand else {
720 return Ok(());
723 };
724
725 let r = resolve_layout(d.layout.as_ref(), section, operand)?;
726 match strategy {
727 "handles_strided" => {
728 if !matches!(r.strided, Tri::Accepted) {
729 return Err(FkcError::AwkwardStrategyIncoherent {
730 section: section.to_string(),
731 operand: operand.to_string(),
732 strategy: strategy.to_string(),
733 reason: "`handles_strided` requires `strided: accepted` on this operand"
734 .to_string(),
735 });
736 }
737 }
738 "requires_contiguous" => {
739 if !matches!(r.contiguous, Tri::Required) {
740 return Err(FkcError::AwkwardStrategyIncoherent {
741 section: section.to_string(),
742 operand: operand.to_string(),
743 strategy: strategy.to_string(),
744 reason: "`requires_contiguous` requires `contiguous: required` on this operand"
745 .to_string(),
746 });
747 }
748 }
749 "contiguize_internally" => {
750 if !matches!(r.strided, Tri::Accepted) {
754 return Err(FkcError::AwkwardStrategyIncoherent {
755 section: section.to_string(),
756 operand: operand.to_string(),
757 strategy: strategy.to_string(),
758 reason: "`contiguize_internally` requires `strided: accepted` on this operand \
759 (it accepts strided then copies internally)"
760 .to_string(),
761 });
762 }
763 }
764 other => {
765 return Err(FkcError::AwkwardStrategyIncoherent {
766 section: section.to_string(),
767 operand: operand.to_string(),
768 strategy: other.to_string(),
769 reason: "unknown awkward_layout_strategy (meaning-bearing — §11.1)".to_string(),
770 });
771 }
772 }
773 Ok(())
774}
775
776fn quant_coherence(
779 section: &str,
780 operand: &str,
781 quant: &QuantSpec,
782 accept: &crate::fkc::schema::AcceptBlock,
783) -> Result<(), FkcError> {
784 let family = quant.family.as_deref().unwrap_or("none");
785
786 if !is_fdx_quant_family(family) {
788 return Err(FkcError::FdxTokenNotInTable {
789 section: section.to_string(),
790 field: format!("{operand}.fdx.quant.family"),
791 token: family.to_string(),
792 });
793 }
794 if let Some(g) = quant.granularity.as_deref()
796 && !is_fdx_granularity(g)
797 {
798 return Err(FkcError::FdxTokenNotInTable {
799 section: section.to_string(),
800 field: format!("{operand}.fdx.quant.granularity"),
801 token: g.to_string(),
802 });
803 }
804 if let Some(g) = quant.ggml_dtype.as_deref()
806 && ggml_dtype_code(g).is_none()
807 {
808 return Err(FkcError::QuantIncoherent {
810 section: section.to_string(),
811 operand: operand.to_string(),
812 reason: format!(
813 "ggml_dtype `{g}` is not a real GgmlDType variant (matched by code; \
814 `Q4_K_M` is a GGUF file-format name → use `Q4K`)"
815 ),
816 });
817 }
818
819 if let Some(scale_role) = quant.scale_operand.as_deref() {
826 let names_real = accept
829 .inputs
830 .iter()
831 .any(|i| i.name.as_deref() == Some(scale_role));
832 if !names_real {
833 return Err(FkcError::ScaleDoubleDeclared {
834 section: section.to_string(),
835 operand: operand.to_string(),
836 reason: format!(
837 "scale_operand `{scale_role}` does not name a real accept.inputs role"
838 ),
839 });
840 }
841 if family == "GGML_BLOCK" {
844 return Err(FkcError::ScaleDoubleDeclared {
845 section: section.to_string(),
846 operand: operand.to_string(),
847 reason: "GGML_BLOCK bakes its scale INLINE (per ggml_dtype) — it must NOT also \
848 declare a separate scale_operand"
849 .to_string(),
850 });
851 }
852 }
853
854 match family {
856 "none" | "NONE" => {
857 }
859 "GGML_BLOCK" => {
860 if quant.ggml_dtype.is_none() {
862 return Err(FkcError::QuantIncoherent {
863 section: section.to_string(),
864 operand: operand.to_string(),
865 reason: "GGML_BLOCK requires a `ggml_dtype` (the block format)".to_string(),
866 });
867 }
868 if quant.granularity.is_some() {
869 return Err(FkcError::QuantIncoherent {
870 section: section.to_string(),
871 operand: operand.to_string(),
872 reason: "GGML_BLOCK carries NO granularity (the scale is baked INLINE per \
873 ggml_dtype; no ScalePair, no PerBlock)"
874 .to_string(),
875 });
876 }
877 }
878 "AFFINE_INT" | "AFFINE_FLOAT" => {
879 match quant.granularity.as_deref() {
881 Some(g) if is_registrable_granularity(g) => {}
882 Some("PerBlock") => {
883 return Err(FkcError::QuantIncoherent {
884 section: section.to_string(),
885 operand: operand.to_string(),
886 reason: format!(
887 "{family} granularity must be PerTensor|PerToken|PerChannel \
888 (PerBlock is MX-only)"
889 ),
890 });
891 }
892 _ => {
893 return Err(FkcError::QuantIncoherent {
894 section: section.to_string(),
895 operand: operand.to_string(),
896 reason: format!(
897 "{family} requires granularity ∈ PerTensor|PerToken|PerChannel"
898 ),
899 });
900 }
901 }
902 }
903 "MX" => {
904 if quant.granularity.as_deref() != Some("PerBlock") {
906 return Err(FkcError::QuantIncoherent {
907 section: section.to_string(),
908 operand: operand.to_string(),
909 reason: "MX requires granularity: PerBlock (F8E8M0 per-block scale)"
910 .to_string(),
911 });
912 }
913 return Err(FkcError::MxNotYetRegistrable {
914 section: section.to_string(),
915 family: "MX".to_string(),
916 reason: "no ScaleGranularity::PerBlock target type yet (§6)".to_string(),
917 });
918 }
919 "AFFINE_BLOCK" => {
920 let has_block_shape = quant.block_shape.is_some();
930 let has_separate_scale = quant.scale_operand.is_some();
931 if !has_block_shape && !has_separate_scale {
932 return Err(FkcError::QuantIncoherent {
933 section: section.to_string(),
934 operand: operand.to_string(),
935 reason: "AFFINE_BLOCK requires block geometry — either `block_shape` or a \
936 block-shaped separate `scale_operand` (§10.6)"
937 .to_string(),
938 });
939 }
940 if quant.granularity.as_deref() == Some("PerBlock") {
941 return Err(FkcError::QuantIncoherent {
942 section: section.to_string(),
943 operand: operand.to_string(),
944 reason: "AFFINE_BLOCK must NOT use granularity: PerBlock (PerBlock stays \
945 MX-only; its grain rides block_shape)"
946 .to_string(),
947 });
948 }
949 return Err(FkcError::MxNotYetRegistrable {
950 section: section.to_string(),
951 family: "AFFINE_BLOCK".to_string(),
952 reason: "no block-quant descriptor target type yet (§6)".to_string(),
953 });
954 }
955 _ => unreachable!("family membership already checked against the FDX table"),
956 }
957
958 Ok(())
959}
960
961fn gather_coherence(
964 section: &str,
965 operand: &str,
966 gather: &crate::fkc::schema::GatherSpec,
967 fdx: &crate::fkc::schema::FdxSpec,
968 accept: &crate::fkc::schema::AcceptBlock,
969) -> Result<(), FkcError> {
970 let Some(kind) = gather.kind.as_deref() else {
971 return Ok(());
973 };
974 match kind {
975 "paged_blocks" => {
976 if fdx.requires_ext != Some(true) {
978 return Err(FkcError::GatherIncoherent {
979 section: section.to_string(),
980 operand: operand.to_string(),
981 reason: "paged_blocks requires `fdx.requires_ext: true`".to_string(),
982 });
983 }
984 if fdx.symbolic_extent.as_deref() != Some("required") {
986 return Err(FkcError::GatherIncoherent {
987 section: section.to_string(),
988 operand: operand.to_string(),
989 reason: "paged_blocks requires `fdx.symbolic_extent: required`".to_string(),
990 });
991 }
992 for (label, role) in [
994 ("block_table", gather.block_table.as_deref()),
995 ("context_lens", gather.context_lens.as_deref()),
996 ] {
997 if let Some(role) = role {
998 let exists = accept
999 .inputs
1000 .iter()
1001 .any(|i| i.name.as_deref() == Some(role));
1002 if !exists {
1003 return Err(FkcError::GatherIncoherent {
1004 section: section.to_string(),
1005 operand: operand.to_string(),
1006 reason: format!(
1007 "gather.{label} `{role}` does not name a real accept.inputs role"
1008 ),
1009 });
1010 }
1011 }
1012 }
1013 Ok(())
1033 }
1034 other => Err(FkcError::UnknownAdmissibilityEnum {
1035 section: section.to_string(),
1036 field: format!("{operand}.fdx.gather.kind"),
1037 value: other.to_string(),
1038 }),
1039 }
1040}
1041
1042fn extent_coherence(
1045 section: &str,
1046 operand: &str,
1047 fdx: &crate::fkc::schema::FdxSpec,
1048) -> Result<(), FkcError> {
1049 if let Some(se) = fdx.symbolic_extent.as_deref()
1051 && !matches!(se, "rejected" | "tolerated" | "required")
1052 {
1053 return Err(FkcError::UnknownAdmissibilityEnum {
1054 section: section.to_string(),
1055 field: format!("{operand}.fdx.symbolic_extent"),
1056 value: se.to_string(),
1057 });
1058 }
1059 if let Some(ek) = fdx.extent_kind.as_deref() {
1061 if !matches!(ek, "rejected" | "scalar" | "range" | "affine") {
1062 return Err(FkcError::UnknownAdmissibilityEnum {
1063 section: section.to_string(),
1064 field: format!("{operand}.fdx.extent_kind"),
1065 value: ek.to_string(),
1066 });
1067 }
1068 if matches!(ek, "range" | "affine") && fdx.symbolic_extent.as_deref() != Some("required") {
1070 return Err(FkcError::UnknownAdmissibilityEnum {
1071 section: section.to_string(),
1072 field: format!("{operand}.fdx.extent_kind"),
1073 value: format!(
1074 "{ek} requires symbolic_extent: required (got {:?})",
1075 fdx.symbolic_extent
1076 ),
1077 });
1078 }
1079 }
1080 Ok(())
1081}
1082
1083fn validate_op_params_namespace(
1091 section: &str,
1092 variant: &str,
1093 is_fused: bool,
1094) -> Result<(), FkcError> {
1095 let ok = if is_fused {
1096 is_fused_op_params_variant(variant)
1097 } else {
1098 is_op_params_variant(variant)
1099 };
1100 if ok {
1101 Ok(())
1102 } else {
1103 Err(FkcError::BadOpParamsVariant {
1104 section: section.to_string(),
1105 variant: variant.to_string(),
1106 namespace: if is_fused {
1107 "FusedOpParams"
1108 } else {
1109 "OpParams"
1110 }
1111 .to_string(),
1112 })
1113 }
1114}
1115
1116macro_rules! variant_table {
1140 ($enum:ident, $guard:ident, $table:ident, $($v:ident => $excluded:expr),+ $(,)?) => {
1141 fn $guard(p: &$enum) {
1145 match p { $( $enum::$v { .. } => (), )+ }
1146 }
1147
1148 const $table: &[(&str, Option<&str>)] = &[ $( (stringify!($v), $excluded) ),+ ];
1150 };
1151}
1152
1153variant_table!(
1154 OpParams,
1155 _op_params_is_exhaustive,
1156 OP_PARAMS_VARIANTS,
1157 None => None,
1158 Reduce => None,
1159 Matmul => None,
1160 Conv1D => None,
1161 Conv2D => None,
1162 ConvTranspose1D => None,
1163 ConvTranspose2D => None,
1164 ReduceSumTo => None,
1165 ReduceMaxTo => None,
1166 ReduceMaxToBackward => None,
1167 FlashAttn => None,
1168 PagedAttn => None,
1169 Slice => None,
1170 Cast => None,
1171 Affine => None,
1172 Clamp => None,
1173 PowI => None,
1174 Concat => None,
1175 SoftmaxLastDim => None,
1176 NormLastDim => None,
1177 IndexSelect => None,
1178 Gather => None,
1179 Rope => None,
1180 QMatMul => None,
1181 IndexAdd => None,
1182 ScatterAdd => None,
1183 Flip => None,
1184 Roll => None,
1185 CumSum => None,
1186 Triangular => None,
1187 LogSoftmaxLastDim => None,
1188 MaskedFill => None,
1189 PadBackward => None,
1190 Pad => None,
1191 WriteSlice => None,
1192 WriteSliceRotating => None,
1193 WriteSliceDoff => None,
1194 FusedSoftmaxCrossEntropy => None,
1195 CausalConv1d => None,
1196 SelectiveScan => None,
1197 SsdChunkScan => None,
1198 Nf4Matmul => None,
1199 JitScalars => Some("runtime-JIT-only: no checked-in kernel to contract"),
1204 NonZeroIndices => None,
1205);
1206
1207variant_table!(
1208 FusedOpParams,
1209 _fused_op_params_is_exhaustive,
1210 FUSED_OP_PARAMS_VARIANTS,
1211 SoftmaxLastDim => None,
1212 FusedLinear => None,
1213 RmsNormLastDim => None,
1214 LayerNormLastDim => None,
1215 Rope => None,
1216 Conv2D => None,
1217 SoftmaxLastDimBackward => None,
1218 LayerNormLastDimBackward => None,
1219 RmsNormLastDimBackward => None,
1220 ReduceMaxToBackward => None,
1221 PowIBackward => None,
1222 ConvTranspose2D => None,
1223 FlashAttn => None,
1224 PagedAttn => None,
1225 QMatMul => None,
1226 InplaceAffine => None,
1227 SsdChunkScan => None,
1228 Nf4Matmul => None,
1229 AwqMatmul => None,
1230 FlashAttnBackward => None,
1231 SelectiveScan => None,
1232 CausalConv1d => None,
1233 FusedSoftmaxCrossEntropy => None,
1234 Runtime => Some("runtime-registered: identity is a runtime FusedOpId"),
1239);
1240
1241fn is_op_params_variant(v: &str) -> bool {
1245 contract_authorable(OP_PARAMS_VARIANTS, v)
1246}
1247
1248fn is_fused_op_params_variant(v: &str) -> bool {
1252 contract_authorable(FUSED_OP_PARAMS_VARIANTS, v)
1253}
1254
1255fn contract_authorable(table: &[(&str, Option<&str>)], v: &str) -> bool {
1260 table
1261 .iter()
1262 .any(|(name, excluded)| *name == v && excluded.is_none())
1263}
1264
1265fn check_output_dtype_rule(section: &str, operand: &str, rule: &str) -> Result<(), FkcError> {
1272 let rule = rule.trim();
1273 if let Some(inner) = rule
1274 .strip_prefix("fixed(")
1275 .and_then(|s| s.strip_suffix(")"))
1276 {
1277 let tok = inner.trim();
1278 lower::lower_dtype(tok, section, operand).map_err(|_| FkcError::FdxTokenNotInTable {
1279 section: section.to_string(),
1280 field: format!("{operand}.dtype_rule fixed(...)"),
1281 token: tok.to_string(),
1282 })?;
1283 }
1284 Ok(())
1285}
1286
1287fn check_bundle_ranks(section: &str, bundle: &serde_yaml_ng::Value) -> Result<(), FkcError> {
1295 let serde_yaml_ng::Value::Sequence(slots) = bundle else {
1296 return Ok(());
1297 };
1298 for (i, slot) in slots.iter().enumerate() {
1299 let serde_yaml_ng::Value::Mapping(map) = slot else {
1300 continue;
1301 };
1302 let slot_name = map
1303 .get(serde_yaml_ng::Value::String("name".into()))
1304 .and_then(|v| v.as_str())
1305 .map(str::to_string)
1306 .unwrap_or_else(|| format!("slot{i}"));
1307 if let Some(serde_yaml_ng::Value::Sequence(dims)) =
1309 map.get(serde_yaml_ng::Value::String("shape".into()))
1310 && dims.len() > 6
1311 {
1312 return Err(FkcError::BundleSlotRankExceeded {
1313 section: section.to_string(),
1314 slot: slot_name,
1315 rank: dims.len(),
1316 });
1317 }
1318 }
1319 Ok(())
1320}
1321
1322fn validate_cost(kernel: &FkcKernel, section: &str) -> Result<(), FkcError> {
1327 let Some(cost) = &kernel.cost else {
1328 return Ok(());
1330 };
1331
1332 match cost.provenance.as_deref() {
1334 Some("declared") | Some("judge_measured") => {}
1335 other => {
1336 return Err(FkcError::CostProvenanceMissing {
1337 section: section.to_string(),
1338 found: other.map(String::from),
1339 });
1340 }
1341 }
1342
1343 use crate::fkc::cost_expr;
1346 let parse = |field: &str, src: Option<&str>| -> Result<(), FkcError> {
1347 cost_expr::compile_field(src)
1348 .map(|_| ())
1349 .map_err(|e| FkcError::CostExprParse {
1350 section: section.to_string(),
1351 field: field.to_string(),
1352 expr: src.unwrap_or("").to_string(),
1353 reason: e.to_string(),
1354 })
1355 };
1356 parse("flops", cost.flops.as_deref())?;
1357 parse("bytes_moved", cost.bytes_moved.as_deref())?;
1358 if let Some(serde_yaml_ng::Value::String(s)) = &cost.overhead_ns {
1359 parse("overhead_ns", Some(s))?;
1360 }
1361 if let Some(mem) = &cost.memory {
1362 for (field, v) in [
1363 ("memory.device_bytes", &mem.device_bytes),
1364 ("memory.host_bytes", &mem.host_bytes),
1365 ("memory.disk_bytes", &mem.disk_bytes),
1366 ] {
1367 if let Some(serde_yaml_ng::Value::String(s)) = v {
1368 parse(field, Some(s))?;
1369 }
1370 }
1371 }
1372
1373 let class = cost.class.as_deref().unwrap_or("");
1385 let has_any_expr = cost.flops.as_deref().is_some_and(|s| !s.trim().is_empty())
1386 || cost
1387 .bytes_moved
1388 .as_deref()
1389 .is_some_and(|s| !s.trim().is_empty());
1390 let has_cost_fn = cost
1391 .cost_fn
1392 .as_deref()
1393 .is_some_and(|s| !s.trim().is_empty());
1394 if cost.provenance.as_deref() == Some("declared")
1395 && crate::fkc::cost_compile::classify_cost("declared", class, has_any_expr, has_cost_fn)
1396 .is_none()
1397 {
1398 return Err(FkcError::PlaceholderCost {
1399 section: section.to_string(),
1400 reason: "provenance: declared with no usable cost path (no flops/bytes_moved \
1401 expression, no pinned cost.cost_fn) and class is not `free`"
1402 .to_string(),
1403 });
1404 }
1405
1406 Ok(())
1407}
1408
1409fn validate_precision_coherence(kernel: &FkcKernel, section: &str) -> Result<(), FkcError> {
1414 let det = kernel.determinism.as_deref().unwrap_or("");
1415 if det == "nondeterministic" {
1416 if let Some(p) = &kernel.precision {
1419 if p.bit_stable_on_same_hardware == Some(true) {
1420 return Err(FkcError::QuantIncoherent {
1421 section: section.to_string(),
1422 operand: "<precision>".to_string(),
1423 reason: "determinism: nondeterministic requires bit_stable_on_same_hardware = \
1424 false"
1425 .to_string(),
1426 });
1427 }
1428 if p.audited != Some(true) {
1429 return Err(FkcError::QuantIncoherent {
1430 section: section.to_string(),
1431 operand: "<precision>".to_string(),
1432 reason: "determinism: nondeterministic requires audited: true with a \
1433 none(reason) precision (no silent unaudited nondeterminism)"
1434 .to_string(),
1435 });
1436 }
1437 }
1438 }
1439 Ok(())
1440}
1441
1442#[cfg(test)]
1443mod tests {
1444 use super::*;
1445 use crate::fkc::parse_file;
1446
1447 fn valid_bundle() -> String {
1452 r#"---
1453fkc_version: 1
1454provider:
1455 name: test-provider
1456 backend: Cpu
1457 kernel_source: "test-cpu"
1458---
1459
1460# test bundle
1461
1462## demo
1463
1464A blurb.
1465
1466```fkc
1467kernel: demo
1468op_kind: AddElementwise
1469blurb: "demo blurb"
1470entry_point: "x::y"
1471accept:
1472 inputs:
1473 - name: lhs
1474 dtypes: [F32]
1475 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
1476 - name: rhs
1477 dtypes: [F32]
1478 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
1479 op_params: { variant: None }
1480return:
1481 outputs:
1482 - name: out
1483 dtype_rule: passthrough(lhs)
1484caps:
1485 awkward_layout_strategy: requires_contiguous
1486cost:
1487 provenance: declared
1488 class: cheap_elementwise
1489 flops: "n"
1490 bytes_moved: "3 * n * 4"
1491precision:
1492 bit_stable_on_same_hardware: true
1493 audited: true
1494determinism: same_hardware_bitwise
1495```
1496"#
1497 .to_string()
1498 }
1499
1500 fn validate_str(src: &str) -> Result<(), FkcError> {
1501 let file = parse_file(src).expect("fixture parses");
1502 validate_file(&file)
1503 }
1504
1505 #[test]
1506 fn valid_contract_passes() {
1507 validate_str(&valid_bundle()).expect("the valid fixture validates");
1508 }
1509
1510 #[test]
1512 fn version_too_new_is_unsupported() {
1513 let src = valid_bundle().replace("fkc_version: 1", "fkc_version: 99");
1514 let err = validate_str(&src).expect_err("version 99 unsupported");
1515 assert!(
1516 matches!(err, FkcError::UnsupportedVersion { found: 99, .. }),
1517 "got {err:?}"
1518 );
1519 }
1520
1521 #[test]
1530 fn fkc_version_zero_is_rejected() {
1531 let src = valid_bundle().replace("fkc_version: 1", "fkc_version: 0");
1532 let err = validate_str(&src).expect_err("fkc_version 0 must be rejected");
1533 assert!(
1534 matches!(err, FkcError::UnsupportedVersion { found: 0, .. }),
1535 "expected UnsupportedVersion for fkc_version 0, got {err:?}"
1536 );
1537 }
1538
1539 #[test]
1544 fn fkc_version_one_is_still_accepted() {
1545 let result = validate_str(&valid_bundle());
1546 assert!(
1547 !matches!(result, Err(FkcError::UnsupportedVersion { .. })),
1548 "fkc_version 1 (sole supported) must pass the version gate, got {result:?}"
1549 );
1550 }
1551
1552 #[test]
1560 fn fkc_version_is_decoupled_from_crate_semver() {
1561 assert_eq!(FKC_SUPPORTED_VERSIONS, &[1]);
1564 }
1565
1566 #[test]
1573 fn fkc_front_matter_schema_is_pinned_to_the_version() {
1574 const PINNED_FKC_SCHEMA_VERSION: u32 = 1;
1575 assert_eq!(
1576 FKC_SUPPORTED_VERSIONS.iter().copied().max(),
1577 Some(PINNED_FKC_SCHEMA_VERSION),
1578 "the latest supported fkc_version moved without updating the pinned \
1579 front-matter schema (or vice-versa) — a §8-0002 schema change bumps both"
1580 );
1581 }
1582
1583 #[test]
1585 fn missing_blurb_errors() {
1586 let src = valid_bundle().replace("blurb: \"demo blurb\"\n", "");
1587 let err = validate_str(&src).expect_err("no blurb");
1588 assert!(matches!(err, FkcError::MissingBlurb { .. }), "got {err:?}");
1589 }
1590
1591 #[test]
1592 fn missing_cost_block_errors() {
1593 let src = valid_bundle();
1595 let cut = src.replace(
1596 "cost:\n provenance: declared\n class: cheap_elementwise\n flops: \"n\"\n bytes_moved: \"3 * n * 4\"\n",
1597 "",
1598 );
1599 let err = validate_str(&cut).expect_err("no cost block");
1600 assert!(
1601 matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "cost"),
1602 "got {err:?}"
1603 );
1604 }
1605
1606 #[test]
1607 fn missing_determinism_errors() {
1608 let src = valid_bundle().replace("determinism: same_hardware_bitwise\n", "");
1609 let err = validate_str(&src).expect_err("no determinism");
1610 assert!(
1611 matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "determinism"),
1612 "got {err:?}"
1613 );
1614 }
1615
1616 #[test]
1617 fn both_op_kind_and_fused_op_is_ambiguous() {
1618 let src = valid_bundle().replace(
1619 "op_kind: AddElementwise",
1620 "op_kind: AddElementwise\nfused_op: SOFTMAX_LAST_DIM",
1621 );
1622 let err = validate_str(&src).expect_err("both targets");
1623 assert!(
1624 matches!(err, FkcError::OpTargetAmbiguous { .. }),
1625 "got {err:?}"
1626 );
1627 }
1628
1629 #[test]
1631 fn bad_dtype_token_is_fdx_token_not_in_table() {
1632 let src = valid_bundle().replace("dtypes: [F32]", "dtypes: [F99]");
1633 let err = validate_str(&src).expect_err("F99 not a dtype");
1634 assert!(
1635 matches!(err, FkcError::FdxTokenNotInTable { .. }),
1636 "got {err:?}"
1637 );
1638 }
1639
1640 #[test]
1642 fn layout_no_acceptable_form_is_incoherent() {
1643 let src = valid_bundle().replace(
1645 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1646 "layout: { contiguous: \"n/a\", strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1647 );
1648 let err = validate_str(&src).expect_err("no acceptable layout");
1649 assert!(
1650 matches!(err, FkcError::LayoutIncoherent { .. }),
1651 "got {err:?}"
1652 );
1653 }
1654
1655 #[test]
1656 fn broadcast_without_strided_is_incoherent() {
1657 let src = valid_bundle().replace(
1658 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1659 "layout: { contiguous: required, strided: rejected, broadcast_stride0: accepted, start_offset: rejected, reverse_strides: rejected }",
1660 );
1661 let err = validate_str(&src).expect_err("broadcast w/o strided");
1662 assert!(
1663 matches!(err, FkcError::LayoutIncoherent { .. }),
1664 "got {err:?}"
1665 );
1666 }
1667
1668 #[test]
1669 fn reverse_without_strided_is_incoherent() {
1670 let src = valid_bundle().replace(
1671 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1672 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: accepted }",
1673 );
1674 let err = validate_str(&src).expect_err("reverse w/o strided");
1675 assert!(
1676 matches!(err, FkcError::LayoutIncoherent { .. }),
1677 "got {err:?}"
1678 );
1679 }
1680
1681 #[test]
1683 fn required_broadcast_with_axes_is_valid() {
1684 let src = valid_bundle()
1686 .replace(
1687 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1688 "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, broadcast_axes: [0], start_offset: rejected, reverse_strides: rejected }",
1689 )
1690 .replace(
1691 "awkward_layout_strategy: requires_contiguous",
1692 "awkward_layout_strategy: handles_strided",
1693 );
1694 validate_str(&src)
1695 .expect("required broadcast with a non-empty broadcast_axes mask validates");
1696 }
1697
1698 #[test]
1699 fn required_broadcast_without_axes_is_incoherent() {
1700 let src = valid_bundle().replace(
1701 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1702 "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, start_offset: rejected, reverse_strides: rejected }",
1703 );
1704 let err = validate_str(&src).expect_err("required broadcast needs a broadcast_axes mask");
1705 assert!(
1706 matches!(err, FkcError::LayoutIncoherent { .. }),
1707 "got {err:?}"
1708 );
1709 }
1710
1711 #[test]
1712 fn broadcast_axes_without_required_is_incoherent() {
1713 let src = valid_bundle().replace(
1714 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1715 "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: accepted, broadcast_axes: [0], start_offset: rejected, reverse_strides: rejected }",
1716 );
1717 let err = validate_str(&src).expect_err("broadcast_axes is meaningful only with required");
1718 assert!(
1719 matches!(err, FkcError::LayoutIncoherent { .. }),
1720 "got {err:?}"
1721 );
1722 }
1723
1724 #[test]
1725 fn required_broadcast_with_empty_axes_is_incoherent() {
1726 let src = valid_bundle().replace(
1727 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1728 "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, broadcast_axes: [], start_offset: rejected, reverse_strides: rejected }",
1729 );
1730 let err = validate_str(&src).expect_err("empty broadcast_axes on a required broadcast");
1731 assert!(
1732 matches!(err, FkcError::LayoutIncoherent { .. }),
1733 "got {err:?}"
1734 );
1735 }
1736
1737 #[test]
1739 fn handles_strided_without_strided_accepted_is_incoherent() {
1740 let src = valid_bundle().replace(
1741 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1742 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected, awkward_layout_strategy: handles_strided }",
1743 );
1744 let err = validate_str(&src).expect_err("handles_strided but strided rejected");
1745 assert!(
1746 matches!(err, FkcError::AwkwardStrategyIncoherent { .. }),
1747 "got {err:?}"
1748 );
1749 }
1750
1751 #[test]
1752 fn unknown_awkward_strategy_is_typed_error() {
1753 let src = valid_bundle().replace(
1754 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1755 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected, awkward_layout_strategy: teleports }",
1756 );
1757 let err = validate_str(&src).expect_err("unknown strategy");
1758 assert!(
1759 matches!(err, FkcError::AwkwardStrategyIncoherent { .. }),
1760 "got {err:?}"
1761 );
1762 }
1763
1764 fn quant_bundle(family: &str, extra: &str) -> String {
1766 format!(
1768 r#"---
1769fkc_version: 1
1770provider:
1771 name: test-provider
1772 backend: Cpu
1773 kernel_source: "test-cpu"
1774---
1775
1776# q
1777
1778## q
1779
1780blurb.
1781
1782```fkc
1783kernel: q
1784op_kind: QMatMul
1785blurb: "q"
1786entry_point: "x::q"
1787accept:
1788 inputs:
1789 - name: act
1790 dtypes: [F32]
1791 layout: {{ contiguous: required, strided: rejected }}
1792 - name: weight
1793 dtypes: [U8]
1794 layout: {{ contiguous: required, strided: rejected }}
1795 fdx:
1796 requires_ext: true
1797 quant:
1798 family: {family}
1799{extra}
1800 op_params: {{ variant: QMatMul }}
1801return:
1802 outputs:
1803 - name: out
1804 dtype_rule: fixed(F32)
1805cost:
1806 provenance: judge_measured
1807 class: gemm_like
1808precision:
1809 bit_stable_on_same_hardware: true
1810 audited: true
1811determinism: same_hardware_bitwise
1812```
1813"#
1814 )
1815 }
1816
1817 #[test]
1818 fn ggml_block_with_real_dtype_ok() {
1819 let src = quant_bundle(
1820 "GGML_BLOCK",
1821 " ggml_dtype: Q4_0\n role: weight",
1822 );
1823 validate_str(&src).expect("GGML_BLOCK Q4_0 weight validates");
1824 }
1825
1826 #[test]
1827 fn ggml_dtype_q4_k_m_is_quant_incoherent() {
1828 let src = quant_bundle(
1830 "GGML_BLOCK",
1831 " ggml_dtype: Q4_K_M\n role: weight",
1832 );
1833 let err = validate_str(&src).expect_err("Q4_K_M bad");
1834 assert!(
1835 matches!(err, FkcError::QuantIncoherent { .. }),
1836 "got {err:?}"
1837 );
1838 }
1839
1840 #[test]
1841 fn ggml_block_with_granularity_is_incoherent() {
1842 let src = quant_bundle(
1843 "GGML_BLOCK",
1844 " ggml_dtype: Q4_0\n granularity: PerChannel\n role: weight",
1845 );
1846 let err = validate_str(&src).expect_err("GGML carries no granularity");
1847 assert!(
1848 matches!(err, FkcError::QuantIncoherent { .. }),
1849 "got {err:?}"
1850 );
1851 }
1852
1853 #[test]
1854 fn mx_family_is_not_yet_registrable() {
1855 let src = quant_bundle(
1856 "MX",
1857 " granularity: PerBlock\n role: weight",
1858 );
1859 let err = validate_str(&src).expect_err("MX not registrable");
1860 assert!(
1861 matches!(err, FkcError::MxNotYetRegistrable { ref family, .. } if family == "MX"),
1862 "got {err:?}"
1863 );
1864 }
1865
1866 #[test]
1867 fn affine_block_without_block_shape_is_incoherent() {
1868 let src = quant_bundle(
1869 "AFFINE_BLOCK",
1870 " role: weight\n scale_operand: scl",
1871 );
1872 let err = validate_str(&src).expect_err("AFFINE_BLOCK needs block_shape / real scale");
1875 assert!(
1876 matches!(
1877 err,
1878 FkcError::QuantIncoherent { .. } | FkcError::ScaleDoubleDeclared { .. }
1879 ),
1880 "got {err:?}"
1881 );
1882 }
1883
1884 #[test]
1885 fn affine_int_bad_granularity_is_incoherent() {
1886 let src = quant_bundle(
1887 "AFFINE_INT",
1888 " granularity: PerBlock\n role: weight",
1889 );
1890 let err = validate_str(&src).expect_err("AFFINE_INT PerBlock bad");
1891 assert!(
1892 matches!(err, FkcError::QuantIncoherent { .. }),
1893 "got {err:?}"
1894 );
1895 }
1896
1897 #[test]
1898 fn unknown_family_is_fdx_token_not_in_table() {
1899 let src = quant_bundle("MADE_UP", " role: weight");
1900 let err = validate_str(&src).expect_err("MADE_UP not an FDX family");
1901 assert!(
1902 matches!(err, FkcError::FdxTokenNotInTable { .. }),
1903 "got {err:?}"
1904 );
1905 }
1906
1907 #[test]
1908 fn ggml_with_separate_scale_operand_is_double_declared() {
1909 let src = quant_bundle(
1912 "GGML_BLOCK",
1913 " ggml_dtype: Q4_0\n role: weight\n scale_operand: act",
1914 );
1915 let err = validate_str(&src).expect_err("GGML + separate scale");
1916 assert!(
1917 matches!(err, FkcError::ScaleDoubleDeclared { .. }),
1918 "got {err:?}"
1919 );
1920 }
1921
1922 #[test]
1924 fn op_kind_with_fused_only_variant_is_bad_namespace() {
1925 let src = valid_bundle().replace(
1928 "op_params: { variant: None }",
1929 "op_params: { variant: FusedLinear }",
1930 );
1931 let err = validate_str(&src).expect_err("FusedLinear not in OpParams");
1932 assert!(
1933 matches!(err, FkcError::BadOpParamsVariant { ref namespace, .. } if namespace == "OpParams"),
1934 "got {err:?}"
1935 );
1936 }
1937
1938 #[test]
1947 fn op_params_namespace_accepts_the_real_nonzeroindices_variant() {
1948 let src = valid_bundle().replace(
1949 "op_params: { variant: None }",
1950 "op_params: { variant: NonZeroIndices }",
1951 );
1952 validate_str(&src)
1953 .expect("NonZeroIndices is a real OpParams variant — a contract may name it");
1954 }
1955
1956 #[test]
1961 fn op_param_predicates_agree_with_the_generated_variant_tables() {
1962 assert!(
1967 OP_PARAMS_VARIANTS.len() >= 40,
1968 "OP_PARAMS_VARIANTS looks truncated ({} entries) — the rest of this \
1969 test would pass vacuously",
1970 OP_PARAMS_VARIANTS.len()
1971 );
1972 assert!(
1973 FUSED_OP_PARAMS_VARIANTS.len() >= 20,
1974 "FUSED_OP_PARAMS_VARIANTS looks truncated ({} entries)",
1975 FUSED_OP_PARAMS_VARIANTS.len()
1976 );
1977
1978 for (name, excluded) in OP_PARAMS_VARIANTS {
1979 assert_eq!(
1980 is_op_params_variant(name),
1981 excluded.is_none(),
1982 "OpParams::{name} — excluded={excluded:?} but the predicate says \
1983 {}",
1984 is_op_params_variant(name)
1985 );
1986 }
1987 for (name, excluded) in FUSED_OP_PARAMS_VARIANTS {
1988 assert_eq!(
1989 is_fused_op_params_variant(name),
1990 excluded.is_none(),
1991 "FusedOpParams::{name} — excluded={excluded:?} but the predicate \
1992 says {}",
1993 is_fused_op_params_variant(name)
1994 );
1995 }
1996
1997 for (name, excluded) in OP_PARAMS_VARIANTS.iter().chain(FUSED_OP_PARAMS_VARIANTS) {
2000 if let Some(reason) = excluded {
2001 assert!(
2002 !reason.trim().is_empty(),
2003 "{name} is excluded with an empty reason"
2004 );
2005 }
2006 }
2007 }
2008
2009 #[test]
2016 fn runtime_only_op_param_variants_stay_out_of_contract_reach() {
2017 assert!(!is_op_params_variant("JitScalars"));
2018 assert!(!is_fused_op_params_variant("Runtime"));
2019
2020 let src = valid_bundle().replace(
2021 "op_params: { variant: None }",
2022 "op_params: { variant: JitScalars }",
2023 );
2024 let err = validate_str(&src).expect_err("JitScalars is runtime-JIT-only");
2025 assert!(
2026 matches!(err, FkcError::BadOpParamsVariant { ref namespace, .. } if namespace == "OpParams"),
2027 "got {err:?}"
2028 );
2029 }
2030
2031 fn fdx_codes_src() -> String {
2053 let path =
2054 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../fuel-ir/src/dlpack/codes.rs");
2055 std::fs::read_to_string(&path).unwrap_or_else(|e| {
2056 panic!(
2057 "the FDX code table must be readable from the workspace (this test checks \
2058 fuel-dispatch against fuel-ir's source text); read {path:?}: {e}"
2059 )
2060 })
2061 }
2062
2063 const FDX_FAMILY_TOKENS: &[(&str, &str)] = &[
2067 ("FDX_QUANT_NONE", "NONE"),
2068 ("FDX_QUANT_GGML_BLOCK", "GGML_BLOCK"),
2069 ("FDX_QUANT_MX", "MX"),
2070 ("FDX_QUANT_AFFINE_INT", "AFFINE_INT"),
2071 ("FDX_QUANT_AFFINE_FLOAT", "AFFINE_FLOAT"),
2072 ("FDX_QUANT_AFFINE_BLOCK", "AFFINE_BLOCK"),
2073 ];
2074
2075 const FDX_GRAN_TOKENS: &[(&str, &str)] = &[
2077 ("FDX_SCALE_GRAN_PER_TENSOR", "PerTensor"),
2078 ("FDX_SCALE_GRAN_PER_TOKEN", "PerToken"),
2079 ("FDX_SCALE_GRAN_PER_CHANNEL", "PerChannel"),
2080 ("FDX_SCALE_GRAN_PER_BLOCK", "PerBlock"),
2081 ];
2082
2083 fn fdx_consts_with_prefix(prefix: &str) -> std::collections::BTreeSet<String> {
2085 fdx_codes_src()
2086 .lines()
2087 .filter_map(|l| l.trim().strip_prefix("pub const ")?.split(':').next())
2088 .map(|n| n.trim().to_string())
2089 .filter(|n| n.starts_with(prefix))
2090 .collect()
2091 }
2092
2093 #[test]
2094 fn fkc_quant_tokens_are_a_faithful_subset_of_the_fdx_code_table() {
2095 let families = fdx_consts_with_prefix("FDX_QUANT_");
2096 let grans = fdx_consts_with_prefix("FDX_SCALE_GRAN_");
2097
2098 assert!(
2104 families.contains("FDX_QUANT_GGML_BLOCK") && grans.contains("FDX_SCALE_GRAN_PER_BLOCK"),
2105 "parsed {} family / {} granularity consts out of {} lines of codes.rs — \
2106 the parse found neither known anchor, so it is broken, not the table",
2107 families.len(),
2108 grans.len(),
2109 fdx_codes_src().lines().count()
2110 );
2111
2112 let declared_families: std::collections::BTreeSet<String> = FDX_FAMILY_TOKENS
2113 .iter()
2114 .map(|(c, _)| c.to_string())
2115 .collect();
2116 assert_eq!(
2117 families, declared_families,
2118 "FDX's FDX_QUANT_* table and FKC's mirror of it have diverged — update \
2119 FDX_FAMILY_TOKENS *and* is_fdx_quant_family together"
2120 );
2121
2122 let declared_grans: std::collections::BTreeSet<String> =
2123 FDX_GRAN_TOKENS.iter().map(|(c, _)| c.to_string()).collect();
2124 assert_eq!(
2125 grans, declared_grans,
2126 "FDX's FDX_SCALE_GRAN_* table and FKC's mirror of it have diverged"
2127 );
2128
2129 for (konst, token) in FDX_FAMILY_TOKENS {
2131 assert!(
2132 is_fdx_quant_family(token),
2133 "FKC rejects `{token}`, the token for the live FDX const {konst}"
2134 );
2135 }
2136 for (konst, token) in FDX_GRAN_TOKENS {
2137 assert!(
2138 is_fdx_granularity(token),
2139 "FKC rejects `{token}`, the token for the live FDX const {konst}"
2140 );
2141 }
2142
2143 assert!(is_fdx_quant_family("none"));
2145
2146 for bogus in ["PerGroup", "AWQ", "GPTQ", "affine_int", "PERTENSOR", ""] {
2149 assert!(!is_fdx_quant_family(bogus), "family accepted `{bogus}`");
2150 assert!(!is_fdx_granularity(bogus), "granularity accepted `{bogus}`");
2151 }
2152 }
2153
2154 #[test]
2156 fn missing_provenance_errors() {
2157 let src = valid_bundle().replace(" provenance: declared\n", "");
2158 let err = validate_str(&src).expect_err("no provenance");
2159 assert!(
2160 matches!(err, FkcError::CostProvenanceMissing { .. }),
2161 "got {err:?}"
2162 );
2163 }
2164
2165 #[test]
2166 fn bad_cost_expr_is_cost_expr_parse() {
2167 let src = valid_bundle().replace("flops: \"n\"", "flops: \"2 * * n\"");
2168 let err = validate_str(&src).expect_err("malformed flops");
2169 assert!(matches!(err, FkcError::CostExprParse { .. }), "got {err:?}");
2170 }
2171
2172 #[test]
2173 fn declared_with_no_coeffs_and_no_class_is_placeholder() {
2174 let src = valid_bundle()
2175 .replace(" class: cheap_elementwise\n", "")
2176 .replace(" flops: \"n\"\n", "")
2177 .replace(" bytes_moved: \"3 * n * 4\"\n", "");
2178 let err = validate_str(&src).expect_err("bare declared cost");
2179 assert!(
2180 matches!(err, FkcError::PlaceholderCost { .. }),
2181 "got {err:?}"
2182 );
2183 }
2184
2185 #[test]
2186 fn judge_measured_all_tilde_is_not_placeholder() {
2187 let src = valid_bundle()
2190 .replace("provenance: declared", "provenance: judge_measured")
2191 .replace(" flops: \"n\"\n", " flops: ~\n")
2192 .replace(" bytes_moved: \"3 * n * 4\"\n", " bytes_moved: ~\n");
2193 validate_str(&src).expect("judge_measured + ~ is fine");
2194 }
2195
2196 #[test]
2197 fn placeholder_cost_class_field() {
2198 use crate::fkc::cost_compile::{CostClassKind, classify_cost};
2199 assert!(classify_cost("declared", "cheap_elementwise", false, false).is_none());
2200 assert!(matches!(
2201 classify_cost("declared", "free", false, false),
2202 Some(CostClassKind::Free)
2203 ));
2204 assert!(matches!(
2205 classify_cost("declared", "gemm_like", true, false),
2206 Some(CostClassKind::DeclaredFormula)
2207 ));
2208 assert!(matches!(
2209 classify_cost("declared", "gemm_like", false, true),
2210 Some(CostClassKind::VendorSpec)
2211 ));
2212 assert!(matches!(
2213 classify_cost("judge_measured", "gemm_like", false, false),
2214 Some(CostClassKind::JudgeMeasured)
2215 ));
2216 use crate::fkc::register::import_bundle_str;
2218 let src = "---\nfkc_version: 1\nprovider:\n name: ph-provider\n backend: Cpu\n kernel_source: \"ph-cpu\"\n---\n\n# ph\n\n## add_f32\n\nA.\n\n```fkc\nkernel: add_f32\nop_kind: AddElementwise\nblurb: \"a\"\nentry_point: \"x::add_f32\"\naccept:\n inputs:\n - name: lhs\n dtypes: [F32]\n layout: { contiguous: required, strided: rejected }\n - name: rhs\n dtypes: [F32]\n layout: { contiguous: required, strided: rejected }\n op_params: { variant: None }\nreturn:\n outputs:\n - name: out\n dtype_rule: passthrough(lhs)\ncost:\n provenance: declared\n class: cheap_elementwise\nprecision:\n bit_stable_on_same_hardware: true\n audited: true\ndeterminism: same_hardware_bitwise\n```\n";
2219 let err = import_bundle_str(src, &StubLink::new())
2220 .expect_err("declared + non-free class + no usable cost path must reject");
2221 assert!(
2222 matches!(err, FkcError::PlaceholderCost { .. }),
2223 "got {err:?}"
2224 );
2225 }
2226
2227 #[test]
2229 fn nondeterministic_with_bitstable_true_is_incoherent() {
2230 let src = valid_bundle().replace(
2231 "determinism: same_hardware_bitwise",
2232 "determinism: nondeterministic",
2233 );
2234 let err = validate_str(&src).expect_err("nondeterministic + bit_stable true");
2235 assert!(
2236 matches!(err, FkcError::QuantIncoherent { .. }),
2237 "got {err:?}"
2238 );
2239 }
2240
2241 #[test]
2243 fn unknown_extent_kind_is_typed_error() {
2244 let src = quant_bundle("none", "").replace(
2245 " fdx:\n requires_ext: true\n quant:\n family: none\n",
2246 " fdx:\n extent_kind: wobbly\n symbolic_extent: required\n",
2247 );
2248 let err = validate_str(&src).expect_err("bad extent_kind");
2249 assert!(
2250 matches!(err, FkcError::UnknownAdmissibilityEnum { .. }),
2251 "got {err:?}"
2252 );
2253 }
2254
2255 fn describe_only_bundle() -> String {
2261 r#"---
2262fkc_version: 1
2263provider:
2264 name: test-provider
2265 backend: Cpu
2266 kernel_source: "test-cpu"
2267---
2268
2269# describe-only bundle
2270
2271## binary
2272
2273A chassis umbrella.
2274
2275```fkc
2276kernel: binary
2277registrable: false
2278op_kind: binary
2279blurb: "shared binary chassis (documentation umbrella)"
2280entry_point: "x::chassis"
2281accept:
2282 inputs:
2283 - name: lhs
2284 dtypes: [F32, F64, BF16, F16]
2285 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2286 - name: rhs
2287 dtypes: [F32, F64, BF16, F16]
2288 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2289return:
2290 outputs:
2291 - name: out
2292 dtype_rule: passthrough(lhs)
2293cost:
2294 provenance: judge_measured
2295 class: cheap_elementwise
2296precision:
2297 bit_stable_on_same_hardware: true
2298 audited: true
2299determinism: same_hardware_bitwise
2300```
2301"#
2302 .to_string()
2303 }
2304
2305 #[test]
2306 fn describe_only_with_descriptive_op_kind_validates() {
2307 validate_str(&describe_only_bundle()).expect("describe-only section validates");
2310 }
2311
2312 #[test]
2313 fn describe_only_with_tilde_op_kind_validates() {
2314 let src = describe_only_bundle().replace("op_kind: binary\n", "op_kind: ~\n");
2317 validate_str(&src).expect("describe-only with op_kind: ~ validates");
2318 }
2319
2320 #[test]
2321 fn non_describe_only_still_requires_real_op_kind_regression() {
2322 let src = describe_only_bundle().replace("registrable: false\n", "");
2326 let err = validate_str(&src).expect_err("registrable section needs a real op_kind");
2327 assert!(matches!(err, FkcError::UnknownOpKind { .. }), "got {err:?}");
2328 }
2329
2330 #[test]
2331 fn describe_only_still_validates_descriptive_dtypes() {
2332 let src = describe_only_bundle().replace("dtypes: [F32, F64, BF16, F16]", "dtypes: [F99]");
2336 let err = validate_str(&src).expect_err("bad dtype in describe-only docs");
2337 assert!(
2338 matches!(err, FkcError::FdxTokenNotInTable { .. }),
2339 "got {err:?}"
2340 );
2341 }
2342
2343 #[test]
2344 fn describe_only_still_validates_layout_coherence() {
2345 let src = describe_only_bundle().replace(
2347 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
2348 "layout: { contiguous: \"n/a\", strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
2349 );
2350 let err = validate_str(&src).expect_err("incoherent layout in describe-only docs");
2351 assert!(
2352 matches!(err, FkcError::LayoutIncoherent { .. }),
2353 "got {err:?}"
2354 );
2355 }
2356
2357 fn describe_only_zero_input_bundle() -> String {
2362 r#"---
2363fkc_version: 1
2364provider:
2365 name: test-provider
2366 backend: Cpu
2367 kernel_source: "test-cpu"
2368---
2369
2370# describe-only zero-input bundle
2371
2372## rand_fill
2373
2374A zero-operand random fill.
2375
2376```fkc
2377kernel: rand_fill
2378registrable: false
2379op_kind: RandUniform
2380blurb: "fill a dense buffer with U(min,max); no input tensor (RNG state is backend-private)"
2381entry_point: "x::rand_fill"
2382accept:
2383 inputs: []
2384return:
2385 outputs:
2386 - name: out
2387 dtype_rule: cast(out)
2388cost:
2389 provenance: judge_measured
2390 class: cheap_elementwise
2391precision:
2392 bit_stable_on_same_hardware: false
2393 audited: true
2394determinism: nondeterministic
2395```
2396"#
2397 .to_string()
2398 }
2399
2400 #[test]
2401 fn describe_only_zero_input_fill_validates() {
2402 validate_str(&describe_only_zero_input_bundle())
2406 .expect("describe-only zero-input fill validates");
2407 }
2408
2409 #[test]
2410 fn registrable_zero_input_still_fails_missing_required_field() {
2411 let src = describe_only_zero_input_bundle()
2416 .replace("registrable: false\n", "")
2417 .replace("op_kind: RandUniform\n", "op_kind: ReluElementwise\n");
2421 let err = validate_str(&src).expect_err("registrable zero-input section is rejected");
2422 assert!(
2423 matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "accept.inputs (≥1)"),
2424 "got {err:?}"
2425 );
2426 }
2427
2428 fn paged_blocks_bundle() -> String {
2439 r#"---
2440fkc_version: 1
2441provider:
2442 name: test-provider
2443 backend: Cpu
2444 kernel_source: "test-cpu"
2445---
2446
2447# paged bundle
2448
2449## paged_demo
2450
2451A paged attention blurb.
2452
2453```fkc
2454kernel: paged_demo
2455op_kind: PagedAttn
2456blurb: "naive paged attention over a blocked KV cache"
2457entry_point: "x::paged_demo"
2458accept:
2459 inputs:
2460 - name: q
2461 dtypes: [F32]
2462 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2463 - name: k_cache
2464 dtypes: [F32]
2465 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2466 fdx:
2467 requires_ext: true
2468 symbolic_extent: required
2469 gather: { kind: paged_blocks, block_table: block_table, context_lens: context_lens }
2470 - name: block_table
2471 dtypes: [U32]
2472 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2473 - name: context_lens
2474 dtypes: [U32]
2475 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2476 op_params: { variant: PagedAttn }
2477return:
2478 outputs:
2479 - name: out
2480 dtype_rule: passthrough(q)
2481cost:
2482 provenance: declared
2483 class: attention
2484 flops: "4 * b * hq * sq * ctx * d"
2485 bytes_moved: "2 * b * hq * sq * d * 4"
2486precision:
2487 bit_stable_on_same_hardware: true
2488 audited: true
2489determinism: same_hardware_bitwise
2490```
2491"#
2492 .to_string()
2493 }
2494
2495 #[test]
2496 fn coherent_registrable_paged_blocks_validates() {
2497 validate_str(&paged_blocks_bundle())
2506 .expect("a coherent registrable paged_blocks section validates + is registrable");
2507 }
2508
2509 #[test]
2510 fn incoherent_gather_missing_requires_ext_still_errors() {
2511 let src = paged_blocks_bundle().replace(" requires_ext: true\n", "");
2515 let err = validate_str(&src).expect_err("gather without requires_ext must error");
2516 assert!(
2517 matches!(err, FkcError::GatherIncoherent { ref reason, .. } if reason.contains("requires_ext")),
2518 "got {err:?}",
2519 );
2520 }
2521
2522 #[test]
2523 fn incoherent_gather_dangling_block_table_role_still_errors() {
2524 let src = paged_blocks_bundle().replace("block_table: block_table,", "block_table: nope,");
2529 let err = validate_str(&src).expect_err("dangling block_table role must error");
2530 assert!(
2531 matches!(err, FkcError::GatherIncoherent { ref reason, .. } if reason.contains("block_table")),
2532 "got {err:?}",
2533 );
2534 }
2535
2536 #[test]
2537 fn unknown_gather_kind_is_unknown_admissibility_enum() {
2538 let src = paged_blocks_bundle().replace("kind: paged_blocks,", "kind: ragged_csr,");
2541 let err = validate_str(&src).expect_err("unknown gather kind must error");
2542 assert!(
2543 matches!(err, FkcError::UnknownAdmissibilityEnum { ref value, .. } if value == "ragged_csr"),
2544 "got {err:?}",
2545 );
2546 }
2547
2548 struct StubLink {
2556 seen: std::sync::Mutex<std::collections::HashMap<String, crate::kernel::KernelRef>>,
2557 }
2558 impl StubLink {
2559 fn new() -> Self {
2560 Self {
2561 seen: std::sync::Mutex::new(std::collections::HashMap::new()),
2562 }
2563 }
2564 fn resolve(&self, symbol: &str) -> Option<crate::kernel::KernelRef> {
2565 use std::sync::Arc;
2566 use std::sync::RwLock;
2567 fn k0(
2571 _i: &[Arc<RwLock<fuel_memory::Storage>>],
2572 _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2573 _l: &[fuel_ir::Layout],
2574 _p: &crate::kernel::OpParams,
2575 ) -> fuel_ir::Result<()> {
2576 Ok(())
2577 }
2578 fn k1(
2579 _i: &[Arc<RwLock<fuel_memory::Storage>>],
2580 _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2581 _l: &[fuel_ir::Layout],
2582 _p: &crate::kernel::OpParams,
2583 ) -> fuel_ir::Result<()> {
2584 Ok(())
2585 }
2586 fn k2(
2587 _i: &[Arc<RwLock<fuel_memory::Storage>>],
2588 _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2589 _l: &[fuel_ir::Layout],
2590 _p: &crate::kernel::OpParams,
2591 ) -> fuel_ir::Result<()> {
2592 Ok(())
2593 }
2594 fn k3(
2595 _i: &[Arc<RwLock<fuel_memory::Storage>>],
2596 _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2597 _l: &[fuel_ir::Layout],
2598 _p: &crate::kernel::OpParams,
2599 ) -> fuel_ir::Result<()> {
2600 Ok(())
2601 }
2602 let table: [crate::kernel::KernelRef; 4] = [k0, k1, k2, k3];
2603 let mut g = self.seen.lock().unwrap();
2604 if let Some(k) = g.get(symbol) {
2605 return Some(*k);
2606 }
2607 let k = table[g.len() % table.len()];
2608 g.insert(symbol.to_string(), k);
2609 Some(k)
2610 }
2611 }
2612 impl crate::fkc::lower::LinkRegistry for StubLink {
2613 fn resolve_primitive(&self, s: &str) -> Option<crate::kernel::KernelRef> {
2614 self.resolve(s)
2615 }
2616 fn resolve_fused(&self, s: &str) -> Option<crate::kernel::KernelRef> {
2617 self.resolve(s)
2618 }
2619 fn resolve_cost_fn(&self, _name: &str) -> Option<crate::kernel::CostFn> {
2620 fn c(
2626 _s: &[fuel_ir::Shape],
2627 _d: &[fuel_ir::DType],
2628 _p: &crate::kernel::OpParams,
2629 _c: &fuel_ir::backend::BackendCapabilities,
2630 ) -> crate::fused::CostEstimate {
2631 crate::fused::CostEstimate::default()
2632 }
2633 Some(c)
2634 }
2635 }
2636
2637 fn collect_fkc_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
2640 let rd = std::fs::read_dir(dir).unwrap_or_else(|e| {
2645 panic!(
2646 "collect_fkc_files: read_dir({}) failed: {e} — the FKC file sweep must FAIL, not silently truncate",
2647 dir.display()
2648 )
2649 });
2650 for entry in rd.flatten() {
2651 let path = entry.path();
2652 if path.is_dir() {
2653 if path.file_name().and_then(|n| n.to_str()) == Some("_inventory") {
2654 continue;
2655 }
2656 collect_fkc_files(&path, out);
2657 } else if path.to_string_lossy().ends_with(".fkc.md") {
2658 out.push(path);
2659 }
2660 }
2661 }
2662
2663 #[test]
2691 fn ci_lint_corpus_parse_lower_validate() {
2692 let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2695 let corpus = manifest.join("kernel-contracts");
2696 assert!(
2697 corpus.is_dir(),
2698 "corpus dir not found at {}",
2699 corpus.display()
2700 );
2701
2702 let mut files = Vec::new();
2703 collect_fkc_files(&corpus, &mut files);
2704 files.sort();
2705 assert!(!files.is_empty(), "no .fkc.md files found");
2706
2707 let link = StubLink::new();
2708 let mut file_count = 0usize;
2709 let mut section_count = 0usize;
2710 let mut hard_failures: Vec<String> = Vec::new();
2711 let mut deferred: Vec<String> = Vec::new();
2712
2713 for path in &files {
2714 file_count += 1;
2715 let rel = path
2716 .strip_prefix(manifest)
2717 .unwrap_or(path)
2718 .display()
2719 .to_string();
2720 let src = std::fs::read_to_string(path).expect("read corpus file");
2721
2722 let file = match parse_file(&src) {
2724 Ok(f) => f,
2725 Err(e) => {
2726 hard_failures.push(format!("{rel}: PARSE: {e}"));
2727 continue;
2728 }
2729 };
2730 section_count += file.kernels.len();
2731
2732 if !fkc_version_is_supported(file.front_matter.fkc_version) {
2736 hard_failures.push(format!(
2737 "{rel}: fkc_version {} not in supported set {:?}",
2738 file.front_matter.fkc_version, FKC_SUPPORTED_VERSIONS
2739 ));
2740 continue;
2741 }
2742 for kernel in &file.kernels {
2743 let kname = kernel.kernel.as_str();
2744 match validate_kernel(kernel) {
2745 Ok(()) => {}
2746 Err(
2747 e @ (FkcError::MxNotYetRegistrable { .. }
2748 | FkcError::GatherNotYetSupported { .. }),
2749 ) => {
2750 deferred.push(format!("{rel} :: {kname}: {e}"));
2751 }
2752 Err(e) => {
2753 hard_failures.push(format!("{rel} :: {kname}: {e}"));
2754 continue;
2755 }
2756 }
2757 if let Err(e) = lower_one_kernel(&file, kernel, &link) {
2761 match e {
2765 FkcError::MxNotYetRegistrable { .. }
2766 | FkcError::GatherNotYetSupported { .. } => {}
2767 FkcError::FanoutDtypeMismatch { .. } => {
2778 deferred.push(format!("{rel} :: {kname}: LOWER: {e}"));
2779 }
2780 FkcError::OptionalOperandNotLast { .. } => {
2791 deferred.push(format!("{rel} :: {kname}: LOWER: {e}"));
2792 }
2793 _ => hard_failures.push(format!("{rel} :: {kname}: LOWER: {e}")),
2794 }
2795 }
2796 }
2797 }
2798
2799 eprintln!(
2800 "FKC corpus lint: {file_count} files, {section_count} sections; {} deferred (MX/gather not-yet-registrable + multi-axis dtype / chained-optional not-yet-fannable), {} hard failures",
2801 deferred.len(),
2802 hard_failures.len()
2803 );
2804 if !deferred.is_empty() {
2805 eprintln!(
2806 "--- deferred (describable, not-yet-registrable / not-yet-fannable; spec §6/§3.9.1/§3.4) ---"
2807 );
2808 for d in &deferred {
2809 eprintln!(" {d}");
2810 }
2811 }
2812 if !hard_failures.is_empty() {
2813 eprintln!("--- HARD FAILURES (file :: kernel: rule) ---");
2814 for f in &hard_failures {
2815 eprintln!(" {f}");
2816 }
2817 panic!(
2818 "{} corpus contract(s) failed parse/lower/validate — see the list above \
2819 (REPORT these, do not relax the validators)",
2820 hard_failures.len()
2821 );
2822 }
2823 }
2824
2825 #[test]
2849 fn ci_lint_corpus_has_no_cross_file_entry_point_collisions() {
2850 let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2851 let corpus = manifest.join("kernel-contracts");
2852 assert!(
2853 corpus.is_dir(),
2854 "corpus dir not found at {}",
2855 corpus.display()
2856 );
2857
2858 let mut files = Vec::new();
2859 collect_fkc_files(&corpus, &mut files);
2860 files.sort();
2861 assert!(!files.is_empty(), "no .fkc.md files found");
2862
2863 let mut claimed: std::collections::HashMap<(String, String), String> =
2865 std::collections::HashMap::new();
2866 let mut collisions: Vec<(String, String, String)> = Vec::new();
2868 let mut checked = 0usize;
2869
2870 for path in &files {
2871 let rel = path
2872 .strip_prefix(manifest)
2873 .unwrap_or(path)
2874 .display()
2875 .to_string();
2876 let src = std::fs::read_to_string(path).expect("read corpus file");
2877 let Ok(file) = parse_file(&src) else {
2878 continue;
2881 };
2882 let kernel_source = file.front_matter.provider.kernel_source.clone();
2883 for kernel in &file.kernels {
2884 if !kernel.registrable {
2887 continue;
2888 }
2889 let Some(entry) = kernel.entry_point.as_ref() else {
2890 continue;
2891 };
2892 checked += 1;
2893 let key = (kernel_source.clone(), entry.clone());
2894 let who = format!("{rel} :: {}", kernel.kernel);
2895 match claimed.get(&key) {
2896 Some(first) => collisions.push((
2897 key.0.clone(),
2898 key.1.clone(),
2899 format!("claimed by BOTH {first} AND {who}"),
2900 )),
2901 None => {
2902 claimed.insert(key, who);
2903 }
2904 }
2905 }
2906 }
2907
2908 eprintln!(
2911 "FKC corpus entry-point uniqueness: {checked} registrable contracts across {} files, {} distinct (kernel_source, entry_point) keys",
2912 files.len(),
2913 claimed.len()
2914 );
2915 assert!(
2916 checked > 0,
2917 "the lint examined zero registrable contracts — it is not looking where it thinks"
2918 );
2919
2920 const KNOWN: &[(&str, &str)] = &[
2937 (
2939 "portable-cpu",
2940 "fuel_cpu_backend::byte_kernels::sum_reduce_f32",
2941 ),
2942 (
2943 "portable-cpu",
2944 "fuel_cpu_backend::byte_kernels::max_reduce_f32",
2945 ),
2946 (
2947 "portable-cpu",
2948 "fuel_cpu_backend::byte_kernels::min_reduce_f32",
2949 ),
2950 (
2951 "portable-cpu",
2952 "fuel_cpu_backend::byte_kernels::mean_reduce_f32",
2953 ),
2954 (
2955 "portable-cpu",
2956 "fuel_cpu_backend::byte_kernels::reduce_sum_to_f32",
2957 ),
2958 (
2959 "portable-cpu",
2960 "fuel_cpu_backend::byte_kernels::reduce_max_to_f32",
2961 ),
2962 (
2963 "portable-cpu",
2964 "fuel_cpu_backend::byte_kernels::reduce_max_to_backward_f32",
2965 ),
2966 ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_f32"),
2968 ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_bf16"),
2969 ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_f16"),
2970 (
2971 "vulkan-slang",
2972 "fuel_vulkan_backend::fkc::flash_attn_backward_q_f32",
2973 ),
2974 (
2975 "vulkan-slang",
2976 "fuel_vulkan_backend::fkc::flash_attn_backward_k_f32",
2977 ),
2978 (
2979 "vulkan-slang",
2980 "fuel_vulkan_backend::fkc::flash_attn_backward_v_f32",
2981 ),
2982 ];
2983 let is_known =
2984 |key: &(String, String)| KNOWN.iter().any(|(ks, ep)| *ks == key.0 && *ep == key.1);
2985
2986 let novel: Vec<&(String, String, String)> = collisions
2987 .iter()
2988 .filter(|c| !is_known(&(c.0.clone(), c.1.clone())))
2989 .collect();
2990
2991 let seen: std::collections::HashSet<(String, String)> = collisions
2995 .iter()
2996 .map(|c| (c.0.clone(), c.1.clone()))
2997 .collect();
2998 let stale: Vec<&(&str, &str)> = KNOWN
2999 .iter()
3000 .filter(|(ks, ep)| !seen.contains(&((*ks).to_string(), (*ep).to_string())))
3001 .collect();
3002
3003 if !stale.is_empty() {
3004 for (ks, ep) in &stale {
3005 eprintln!(" STALE ALLOWLIST ENTRY (no longer collides): {ks} :: {ep}");
3006 }
3007 panic!(
3008 "{} allowlisted collision(s) no longer occur — delete them from KNOWN so the list keeps reflecting the real debt",
3009 stale.len()
3010 );
3011 }
3012
3013 if !novel.is_empty() {
3014 for (ks, ep, who) in &novel {
3015 eprintln!(" NEW COLLISION: kernel_source `{ks}` entry_point `{ep}` — {who}");
3016 }
3017 panic!(
3018 "{} NEW cross-file entry-point collision(s) — each would resolve to one KernelRef and trip the duplicate gate at registration. Give the new contract its own entry point, or mark it `registrable: false` if it is documentation.",
3019 novel.len()
3020 );
3021 }
3022 }
3023
3024 fn lower_one_kernel(
3029 file: &FkcFile,
3030 kernel: &FkcKernel,
3031 link: &dyn crate::fkc::lower::LinkRegistry,
3032 ) -> Result<(), FkcError> {
3033 let mut one = file.clone();
3036 one.kernels = vec![kernel.clone()];
3037 crate::fkc::lower::lower_file(&one, link, &mut Vec::new()).map(|_| ())
3038 }
3039}