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 AwqMatmul => None,
1200 JitScalars => Some("runtime-JIT-only: no checked-in kernel to contract"),
1205 NonZeroIndices => None,
1206);
1207
1208variant_table!(
1209 FusedOpParams,
1210 _fused_op_params_is_exhaustive,
1211 FUSED_OP_PARAMS_VARIANTS,
1212 SoftmaxLastDim => None,
1213 FusedLinear => None,
1214 RmsNormLastDim => None,
1215 LayerNormLastDim => None,
1216 Rope => None,
1217 Conv2D => None,
1218 SoftmaxLastDimBackward => None,
1219 LayerNormLastDimBackward => None,
1220 RmsNormLastDimBackward => None,
1221 ReduceMaxToBackward => None,
1222 PowIBackward => None,
1223 ConvTranspose2D => None,
1224 FlashAttn => None,
1225 PagedAttn => None,
1226 QMatMul => None,
1227 InplaceAffine => None,
1228 SsdChunkScan => None,
1229 Nf4Matmul => None,
1230 AwqMatmul => None,
1231 FlashAttnBackward => None,
1232 SelectiveScan => None,
1233 CausalConv1d => None,
1234 FusedSoftmaxCrossEntropy => None,
1235 Runtime => Some("runtime-registered: identity is a runtime FusedOpId"),
1240);
1241
1242fn is_op_params_variant(v: &str) -> bool {
1246 contract_authorable(OP_PARAMS_VARIANTS, v)
1247}
1248
1249fn is_fused_op_params_variant(v: &str) -> bool {
1253 contract_authorable(FUSED_OP_PARAMS_VARIANTS, v)
1254}
1255
1256fn contract_authorable(table: &[(&str, Option<&str>)], v: &str) -> bool {
1261 table
1262 .iter()
1263 .any(|(name, excluded)| *name == v && excluded.is_none())
1264}
1265
1266fn check_output_dtype_rule(section: &str, operand: &str, rule: &str) -> Result<(), FkcError> {
1273 let rule = rule.trim();
1274 if let Some(inner) = rule
1275 .strip_prefix("fixed(")
1276 .and_then(|s| s.strip_suffix(")"))
1277 {
1278 let tok = inner.trim();
1279 lower::lower_dtype(tok, section, operand).map_err(|_| FkcError::FdxTokenNotInTable {
1280 section: section.to_string(),
1281 field: format!("{operand}.dtype_rule fixed(...)"),
1282 token: tok.to_string(),
1283 })?;
1284 }
1285 Ok(())
1286}
1287
1288fn check_bundle_ranks(section: &str, bundle: &serde_yaml_ng::Value) -> Result<(), FkcError> {
1296 let serde_yaml_ng::Value::Sequence(slots) = bundle else {
1297 return Ok(());
1298 };
1299 for (i, slot) in slots.iter().enumerate() {
1300 let serde_yaml_ng::Value::Mapping(map) = slot else {
1301 continue;
1302 };
1303 let slot_name = map
1304 .get(serde_yaml_ng::Value::String("name".into()))
1305 .and_then(|v| v.as_str())
1306 .map(str::to_string)
1307 .unwrap_or_else(|| format!("slot{i}"));
1308 if let Some(serde_yaml_ng::Value::Sequence(dims)) =
1310 map.get(serde_yaml_ng::Value::String("shape".into()))
1311 && dims.len() > 6
1312 {
1313 return Err(FkcError::BundleSlotRankExceeded {
1314 section: section.to_string(),
1315 slot: slot_name,
1316 rank: dims.len(),
1317 });
1318 }
1319 }
1320 Ok(())
1321}
1322
1323fn validate_cost(kernel: &FkcKernel, section: &str) -> Result<(), FkcError> {
1328 let Some(cost) = &kernel.cost else {
1329 return Ok(());
1331 };
1332
1333 match cost.provenance.as_deref() {
1335 Some("declared") | Some("judge_measured") => {}
1336 other => {
1337 return Err(FkcError::CostProvenanceMissing {
1338 section: section.to_string(),
1339 found: other.map(String::from),
1340 });
1341 }
1342 }
1343
1344 use crate::fkc::cost_expr;
1347 let parse = |field: &str, src: Option<&str>| -> Result<(), FkcError> {
1348 cost_expr::compile_field(src)
1349 .map(|_| ())
1350 .map_err(|e| FkcError::CostExprParse {
1351 section: section.to_string(),
1352 field: field.to_string(),
1353 expr: src.unwrap_or("").to_string(),
1354 reason: e.to_string(),
1355 })
1356 };
1357 parse("flops", cost.flops.as_deref())?;
1358 parse("bytes_moved", cost.bytes_moved.as_deref())?;
1359 if let Some(serde_yaml_ng::Value::String(s)) = &cost.overhead_ns {
1360 parse("overhead_ns", Some(s))?;
1361 }
1362 if let Some(mem) = &cost.memory {
1363 for (field, v) in [
1364 ("memory.device_bytes", &mem.device_bytes),
1365 ("memory.host_bytes", &mem.host_bytes),
1366 ("memory.disk_bytes", &mem.disk_bytes),
1367 ] {
1368 if let Some(serde_yaml_ng::Value::String(s)) = v {
1369 parse(field, Some(s))?;
1370 }
1371 }
1372 }
1373
1374 let class = cost.class.as_deref().unwrap_or("");
1386 let has_any_expr = cost.flops.as_deref().is_some_and(|s| !s.trim().is_empty())
1387 || cost
1388 .bytes_moved
1389 .as_deref()
1390 .is_some_and(|s| !s.trim().is_empty());
1391 let has_cost_fn = cost
1392 .cost_fn
1393 .as_deref()
1394 .is_some_and(|s| !s.trim().is_empty());
1395 if cost.provenance.as_deref() == Some("declared")
1396 && crate::fkc::cost_compile::classify_cost("declared", class, has_any_expr, has_cost_fn)
1397 .is_none()
1398 {
1399 return Err(FkcError::PlaceholderCost {
1400 section: section.to_string(),
1401 reason: "provenance: declared with no usable cost path (no flops/bytes_moved \
1402 expression, no pinned cost.cost_fn) and class is not `free`"
1403 .to_string(),
1404 });
1405 }
1406
1407 Ok(())
1408}
1409
1410fn validate_precision_coherence(kernel: &FkcKernel, section: &str) -> Result<(), FkcError> {
1415 let det = kernel.determinism.as_deref().unwrap_or("");
1416 if det == "nondeterministic" {
1417 if let Some(p) = &kernel.precision {
1420 if p.bit_stable_on_same_hardware == Some(true) {
1421 return Err(FkcError::QuantIncoherent {
1422 section: section.to_string(),
1423 operand: "<precision>".to_string(),
1424 reason: "determinism: nondeterministic requires bit_stable_on_same_hardware = \
1425 false"
1426 .to_string(),
1427 });
1428 }
1429 if p.audited != Some(true) {
1430 return Err(FkcError::QuantIncoherent {
1431 section: section.to_string(),
1432 operand: "<precision>".to_string(),
1433 reason: "determinism: nondeterministic requires audited: true with a \
1434 none(reason) precision (no silent unaudited nondeterminism)"
1435 .to_string(),
1436 });
1437 }
1438 }
1439 }
1440 Ok(())
1441}
1442
1443#[cfg(test)]
1444mod tests {
1445 use super::*;
1446 use crate::fkc::parse_file;
1447
1448 fn valid_bundle() -> String {
1453 r#"---
1454fkc_version: 1
1455provider:
1456 name: test-provider
1457 backend: Cpu
1458 kernel_source: "test-cpu"
1459---
1460
1461# test bundle
1462
1463## demo
1464
1465A blurb.
1466
1467```fkc
1468kernel: demo
1469op_kind: AddElementwise
1470blurb: "demo blurb"
1471entry_point: "x::y"
1472accept:
1473 inputs:
1474 - name: lhs
1475 dtypes: [F32]
1476 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
1477 - name: rhs
1478 dtypes: [F32]
1479 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
1480 op_params: { variant: None }
1481return:
1482 outputs:
1483 - name: out
1484 dtype_rule: passthrough(lhs)
1485caps:
1486 awkward_layout_strategy: requires_contiguous
1487cost:
1488 provenance: declared
1489 class: cheap_elementwise
1490 flops: "n"
1491 bytes_moved: "3 * n * 4"
1492precision:
1493 bit_stable_on_same_hardware: true
1494 audited: true
1495determinism: same_hardware_bitwise
1496```
1497"#
1498 .to_string()
1499 }
1500
1501 fn validate_str(src: &str) -> Result<(), FkcError> {
1502 let file = parse_file(src).expect("fixture parses");
1503 validate_file(&file)
1504 }
1505
1506 #[test]
1507 fn valid_contract_passes() {
1508 validate_str(&valid_bundle()).expect("the valid fixture validates");
1509 }
1510
1511 #[test]
1513 fn version_too_new_is_unsupported() {
1514 let src = valid_bundle().replace("fkc_version: 1", "fkc_version: 99");
1515 let err = validate_str(&src).expect_err("version 99 unsupported");
1516 assert!(
1517 matches!(err, FkcError::UnsupportedVersion { found: 99, .. }),
1518 "got {err:?}"
1519 );
1520 }
1521
1522 #[test]
1531 fn fkc_version_zero_is_rejected() {
1532 let src = valid_bundle().replace("fkc_version: 1", "fkc_version: 0");
1533 let err = validate_str(&src).expect_err("fkc_version 0 must be rejected");
1534 assert!(
1535 matches!(err, FkcError::UnsupportedVersion { found: 0, .. }),
1536 "expected UnsupportedVersion for fkc_version 0, got {err:?}"
1537 );
1538 }
1539
1540 #[test]
1545 fn fkc_version_one_is_still_accepted() {
1546 let result = validate_str(&valid_bundle());
1547 assert!(
1548 !matches!(result, Err(FkcError::UnsupportedVersion { .. })),
1549 "fkc_version 1 (sole supported) must pass the version gate, got {result:?}"
1550 );
1551 }
1552
1553 #[test]
1561 fn fkc_version_is_decoupled_from_crate_semver() {
1562 assert_eq!(FKC_SUPPORTED_VERSIONS, &[1]);
1565 }
1566
1567 #[test]
1574 fn fkc_front_matter_schema_is_pinned_to_the_version() {
1575 const PINNED_FKC_SCHEMA_VERSION: u32 = 1;
1576 assert_eq!(
1577 FKC_SUPPORTED_VERSIONS.iter().copied().max(),
1578 Some(PINNED_FKC_SCHEMA_VERSION),
1579 "the latest supported fkc_version moved without updating the pinned \
1580 front-matter schema (or vice-versa) — a §8-0002 schema change bumps both"
1581 );
1582 }
1583
1584 #[test]
1586 fn missing_blurb_errors() {
1587 let src = valid_bundle().replace("blurb: \"demo blurb\"\n", "");
1588 let err = validate_str(&src).expect_err("no blurb");
1589 assert!(matches!(err, FkcError::MissingBlurb { .. }), "got {err:?}");
1590 }
1591
1592 #[test]
1593 fn missing_cost_block_errors() {
1594 let src = valid_bundle();
1596 let cut = src.replace(
1597 "cost:\n provenance: declared\n class: cheap_elementwise\n flops: \"n\"\n bytes_moved: \"3 * n * 4\"\n",
1598 "",
1599 );
1600 let err = validate_str(&cut).expect_err("no cost block");
1601 assert!(
1602 matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "cost"),
1603 "got {err:?}"
1604 );
1605 }
1606
1607 #[test]
1608 fn missing_determinism_errors() {
1609 let src = valid_bundle().replace("determinism: same_hardware_bitwise\n", "");
1610 let err = validate_str(&src).expect_err("no determinism");
1611 assert!(
1612 matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "determinism"),
1613 "got {err:?}"
1614 );
1615 }
1616
1617 #[test]
1618 fn both_op_kind_and_fused_op_is_ambiguous() {
1619 let src = valid_bundle().replace(
1620 "op_kind: AddElementwise",
1621 "op_kind: AddElementwise\nfused_op: SOFTMAX_LAST_DIM",
1622 );
1623 let err = validate_str(&src).expect_err("both targets");
1624 assert!(
1625 matches!(err, FkcError::OpTargetAmbiguous { .. }),
1626 "got {err:?}"
1627 );
1628 }
1629
1630 #[test]
1632 fn bad_dtype_token_is_fdx_token_not_in_table() {
1633 let src = valid_bundle().replace("dtypes: [F32]", "dtypes: [F99]");
1634 let err = validate_str(&src).expect_err("F99 not a dtype");
1635 assert!(
1636 matches!(err, FkcError::FdxTokenNotInTable { .. }),
1637 "got {err:?}"
1638 );
1639 }
1640
1641 #[test]
1643 fn layout_no_acceptable_form_is_incoherent() {
1644 let src = valid_bundle().replace(
1646 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1647 "layout: { contiguous: \"n/a\", strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1648 );
1649 let err = validate_str(&src).expect_err("no acceptable layout");
1650 assert!(
1651 matches!(err, FkcError::LayoutIncoherent { .. }),
1652 "got {err:?}"
1653 );
1654 }
1655
1656 #[test]
1657 fn broadcast_without_strided_is_incoherent() {
1658 let src = valid_bundle().replace(
1659 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1660 "layout: { contiguous: required, strided: rejected, broadcast_stride0: accepted, start_offset: rejected, reverse_strides: rejected }",
1661 );
1662 let err = validate_str(&src).expect_err("broadcast w/o strided");
1663 assert!(
1664 matches!(err, FkcError::LayoutIncoherent { .. }),
1665 "got {err:?}"
1666 );
1667 }
1668
1669 #[test]
1670 fn reverse_without_strided_is_incoherent() {
1671 let src = valid_bundle().replace(
1672 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1673 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: accepted }",
1674 );
1675 let err = validate_str(&src).expect_err("reverse w/o strided");
1676 assert!(
1677 matches!(err, FkcError::LayoutIncoherent { .. }),
1678 "got {err:?}"
1679 );
1680 }
1681
1682 #[test]
1684 fn required_broadcast_with_axes_is_valid() {
1685 let src = valid_bundle()
1687 .replace(
1688 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1689 "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, broadcast_axes: [0], start_offset: rejected, reverse_strides: rejected }",
1690 )
1691 .replace(
1692 "awkward_layout_strategy: requires_contiguous",
1693 "awkward_layout_strategy: handles_strided",
1694 );
1695 validate_str(&src)
1696 .expect("required broadcast with a non-empty broadcast_axes mask validates");
1697 }
1698
1699 #[test]
1700 fn required_broadcast_without_axes_is_incoherent() {
1701 let src = valid_bundle().replace(
1702 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1703 "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, start_offset: rejected, reverse_strides: rejected }",
1704 );
1705 let err = validate_str(&src).expect_err("required broadcast needs a broadcast_axes mask");
1706 assert!(
1707 matches!(err, FkcError::LayoutIncoherent { .. }),
1708 "got {err:?}"
1709 );
1710 }
1711
1712 #[test]
1713 fn broadcast_axes_without_required_is_incoherent() {
1714 let src = valid_bundle().replace(
1715 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1716 "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: accepted, broadcast_axes: [0], start_offset: rejected, reverse_strides: rejected }",
1717 );
1718 let err = validate_str(&src).expect_err("broadcast_axes is meaningful only with required");
1719 assert!(
1720 matches!(err, FkcError::LayoutIncoherent { .. }),
1721 "got {err:?}"
1722 );
1723 }
1724
1725 #[test]
1726 fn required_broadcast_with_empty_axes_is_incoherent() {
1727 let src = valid_bundle().replace(
1728 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1729 "layout: { contiguous: accepted, strided: accepted, broadcast_stride0: required, broadcast_axes: [], start_offset: rejected, reverse_strides: rejected }",
1730 );
1731 let err = validate_str(&src).expect_err("empty broadcast_axes on a required broadcast");
1732 assert!(
1733 matches!(err, FkcError::LayoutIncoherent { .. }),
1734 "got {err:?}"
1735 );
1736 }
1737
1738 #[test]
1740 fn handles_strided_without_strided_accepted_is_incoherent() {
1741 let src = valid_bundle().replace(
1742 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1743 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected, awkward_layout_strategy: handles_strided }",
1744 );
1745 let err = validate_str(&src).expect_err("handles_strided but strided rejected");
1746 assert!(
1747 matches!(err, FkcError::AwkwardStrategyIncoherent { .. }),
1748 "got {err:?}"
1749 );
1750 }
1751
1752 #[test]
1753 fn unknown_awkward_strategy_is_typed_error() {
1754 let src = valid_bundle().replace(
1755 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
1756 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected, awkward_layout_strategy: teleports }",
1757 );
1758 let err = validate_str(&src).expect_err("unknown strategy");
1759 assert!(
1760 matches!(err, FkcError::AwkwardStrategyIncoherent { .. }),
1761 "got {err:?}"
1762 );
1763 }
1764
1765 fn quant_bundle(family: &str, extra: &str) -> String {
1767 format!(
1769 r#"---
1770fkc_version: 1
1771provider:
1772 name: test-provider
1773 backend: Cpu
1774 kernel_source: "test-cpu"
1775---
1776
1777# q
1778
1779## q
1780
1781blurb.
1782
1783```fkc
1784kernel: q
1785op_kind: QMatMul
1786blurb: "q"
1787entry_point: "x::q"
1788accept:
1789 inputs:
1790 - name: act
1791 dtypes: [F32]
1792 layout: {{ contiguous: required, strided: rejected }}
1793 - name: weight
1794 dtypes: [U8]
1795 layout: {{ contiguous: required, strided: rejected }}
1796 fdx:
1797 requires_ext: true
1798 quant:
1799 family: {family}
1800{extra}
1801 op_params: {{ variant: QMatMul }}
1802return:
1803 outputs:
1804 - name: out
1805 dtype_rule: fixed(F32)
1806cost:
1807 provenance: judge_measured
1808 class: gemm_like
1809precision:
1810 bit_stable_on_same_hardware: true
1811 audited: true
1812determinism: same_hardware_bitwise
1813```
1814"#
1815 )
1816 }
1817
1818 #[test]
1819 fn ggml_block_with_real_dtype_ok() {
1820 let src = quant_bundle(
1821 "GGML_BLOCK",
1822 " ggml_dtype: Q4_0\n role: weight",
1823 );
1824 validate_str(&src).expect("GGML_BLOCK Q4_0 weight validates");
1825 }
1826
1827 #[test]
1828 fn ggml_dtype_q4_k_m_is_quant_incoherent() {
1829 let src = quant_bundle(
1831 "GGML_BLOCK",
1832 " ggml_dtype: Q4_K_M\n role: weight",
1833 );
1834 let err = validate_str(&src).expect_err("Q4_K_M bad");
1835 assert!(
1836 matches!(err, FkcError::QuantIncoherent { .. }),
1837 "got {err:?}"
1838 );
1839 }
1840
1841 #[test]
1842 fn ggml_block_with_granularity_is_incoherent() {
1843 let src = quant_bundle(
1844 "GGML_BLOCK",
1845 " ggml_dtype: Q4_0\n granularity: PerChannel\n role: weight",
1846 );
1847 let err = validate_str(&src).expect_err("GGML carries no granularity");
1848 assert!(
1849 matches!(err, FkcError::QuantIncoherent { .. }),
1850 "got {err:?}"
1851 );
1852 }
1853
1854 #[test]
1855 fn mx_family_is_not_yet_registrable() {
1856 let src = quant_bundle(
1857 "MX",
1858 " granularity: PerBlock\n role: weight",
1859 );
1860 let err = validate_str(&src).expect_err("MX not registrable");
1861 assert!(
1862 matches!(err, FkcError::MxNotYetRegistrable { ref family, .. } if family == "MX"),
1863 "got {err:?}"
1864 );
1865 }
1866
1867 #[test]
1868 fn affine_block_without_block_shape_is_incoherent() {
1869 let src = quant_bundle(
1870 "AFFINE_BLOCK",
1871 " role: weight\n scale_operand: scl",
1872 );
1873 let err = validate_str(&src).expect_err("AFFINE_BLOCK needs block_shape / real scale");
1876 assert!(
1877 matches!(
1878 err,
1879 FkcError::QuantIncoherent { .. } | FkcError::ScaleDoubleDeclared { .. }
1880 ),
1881 "got {err:?}"
1882 );
1883 }
1884
1885 #[test]
1886 fn affine_int_bad_granularity_is_incoherent() {
1887 let src = quant_bundle(
1888 "AFFINE_INT",
1889 " granularity: PerBlock\n role: weight",
1890 );
1891 let err = validate_str(&src).expect_err("AFFINE_INT PerBlock bad");
1892 assert!(
1893 matches!(err, FkcError::QuantIncoherent { .. }),
1894 "got {err:?}"
1895 );
1896 }
1897
1898 #[test]
1899 fn unknown_family_is_fdx_token_not_in_table() {
1900 let src = quant_bundle("MADE_UP", " role: weight");
1901 let err = validate_str(&src).expect_err("MADE_UP not an FDX family");
1902 assert!(
1903 matches!(err, FkcError::FdxTokenNotInTable { .. }),
1904 "got {err:?}"
1905 );
1906 }
1907
1908 #[test]
1909 fn ggml_with_separate_scale_operand_is_double_declared() {
1910 let src = quant_bundle(
1913 "GGML_BLOCK",
1914 " ggml_dtype: Q4_0\n role: weight\n scale_operand: act",
1915 );
1916 let err = validate_str(&src).expect_err("GGML + separate scale");
1917 assert!(
1918 matches!(err, FkcError::ScaleDoubleDeclared { .. }),
1919 "got {err:?}"
1920 );
1921 }
1922
1923 #[test]
1925 fn op_kind_with_fused_only_variant_is_bad_namespace() {
1926 let src = valid_bundle().replace(
1929 "op_params: { variant: None }",
1930 "op_params: { variant: FusedLinear }",
1931 );
1932 let err = validate_str(&src).expect_err("FusedLinear not in OpParams");
1933 assert!(
1934 matches!(err, FkcError::BadOpParamsVariant { ref namespace, .. } if namespace == "OpParams"),
1935 "got {err:?}"
1936 );
1937 }
1938
1939 #[test]
1948 fn op_params_namespace_accepts_the_real_nonzeroindices_variant() {
1949 let src = valid_bundle().replace(
1950 "op_params: { variant: None }",
1951 "op_params: { variant: NonZeroIndices }",
1952 );
1953 validate_str(&src)
1954 .expect("NonZeroIndices is a real OpParams variant — a contract may name it");
1955 }
1956
1957 #[test]
1962 fn op_param_predicates_agree_with_the_generated_variant_tables() {
1963 assert!(
1968 OP_PARAMS_VARIANTS.len() >= 40,
1969 "OP_PARAMS_VARIANTS looks truncated ({} entries) — the rest of this \
1970 test would pass vacuously",
1971 OP_PARAMS_VARIANTS.len()
1972 );
1973 assert!(
1974 FUSED_OP_PARAMS_VARIANTS.len() >= 20,
1975 "FUSED_OP_PARAMS_VARIANTS looks truncated ({} entries)",
1976 FUSED_OP_PARAMS_VARIANTS.len()
1977 );
1978
1979 for (name, excluded) in OP_PARAMS_VARIANTS {
1980 assert_eq!(
1981 is_op_params_variant(name),
1982 excluded.is_none(),
1983 "OpParams::{name} — excluded={excluded:?} but the predicate says \
1984 {}",
1985 is_op_params_variant(name)
1986 );
1987 }
1988 for (name, excluded) in FUSED_OP_PARAMS_VARIANTS {
1989 assert_eq!(
1990 is_fused_op_params_variant(name),
1991 excluded.is_none(),
1992 "FusedOpParams::{name} — excluded={excluded:?} but the predicate \
1993 says {}",
1994 is_fused_op_params_variant(name)
1995 );
1996 }
1997
1998 for (name, excluded) in OP_PARAMS_VARIANTS.iter().chain(FUSED_OP_PARAMS_VARIANTS) {
2001 if let Some(reason) = excluded {
2002 assert!(
2003 !reason.trim().is_empty(),
2004 "{name} is excluded with an empty reason"
2005 );
2006 }
2007 }
2008 }
2009
2010 #[test]
2017 fn runtime_only_op_param_variants_stay_out_of_contract_reach() {
2018 assert!(!is_op_params_variant("JitScalars"));
2019 assert!(!is_fused_op_params_variant("Runtime"));
2020
2021 let src = valid_bundle().replace(
2022 "op_params: { variant: None }",
2023 "op_params: { variant: JitScalars }",
2024 );
2025 let err = validate_str(&src).expect_err("JitScalars is runtime-JIT-only");
2026 assert!(
2027 matches!(err, FkcError::BadOpParamsVariant { ref namespace, .. } if namespace == "OpParams"),
2028 "got {err:?}"
2029 );
2030 }
2031
2032 fn fdx_codes_src() -> String {
2054 let path =
2055 std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../fuel-ir/src/dlpack/codes.rs");
2056 std::fs::read_to_string(&path).unwrap_or_else(|e| {
2057 panic!(
2058 "the FDX code table must be readable from the workspace (this test checks \
2059 fuel-dispatch against fuel-ir's source text); read {path:?}: {e}"
2060 )
2061 })
2062 }
2063
2064 const FDX_FAMILY_TOKENS: &[(&str, &str)] = &[
2068 ("FDX_QUANT_NONE", "NONE"),
2069 ("FDX_QUANT_GGML_BLOCK", "GGML_BLOCK"),
2070 ("FDX_QUANT_MX", "MX"),
2071 ("FDX_QUANT_AFFINE_INT", "AFFINE_INT"),
2072 ("FDX_QUANT_AFFINE_FLOAT", "AFFINE_FLOAT"),
2073 ("FDX_QUANT_AFFINE_BLOCK", "AFFINE_BLOCK"),
2074 ];
2075
2076 const FDX_GRAN_TOKENS: &[(&str, &str)] = &[
2078 ("FDX_SCALE_GRAN_PER_TENSOR", "PerTensor"),
2079 ("FDX_SCALE_GRAN_PER_TOKEN", "PerToken"),
2080 ("FDX_SCALE_GRAN_PER_CHANNEL", "PerChannel"),
2081 ("FDX_SCALE_GRAN_PER_BLOCK", "PerBlock"),
2082 ];
2083
2084 fn fdx_consts_with_prefix(prefix: &str) -> std::collections::BTreeSet<String> {
2086 fdx_codes_src()
2087 .lines()
2088 .filter_map(|l| l.trim().strip_prefix("pub const ")?.split(':').next())
2089 .map(|n| n.trim().to_string())
2090 .filter(|n| n.starts_with(prefix))
2091 .collect()
2092 }
2093
2094 #[test]
2095 fn fkc_quant_tokens_are_a_faithful_subset_of_the_fdx_code_table() {
2096 let families = fdx_consts_with_prefix("FDX_QUANT_");
2097 let grans = fdx_consts_with_prefix("FDX_SCALE_GRAN_");
2098
2099 assert!(
2105 families.contains("FDX_QUANT_GGML_BLOCK") && grans.contains("FDX_SCALE_GRAN_PER_BLOCK"),
2106 "parsed {} family / {} granularity consts out of {} lines of codes.rs — \
2107 the parse found neither known anchor, so it is broken, not the table",
2108 families.len(),
2109 grans.len(),
2110 fdx_codes_src().lines().count()
2111 );
2112
2113 let declared_families: std::collections::BTreeSet<String> = FDX_FAMILY_TOKENS
2114 .iter()
2115 .map(|(c, _)| c.to_string())
2116 .collect();
2117 assert_eq!(
2118 families, declared_families,
2119 "FDX's FDX_QUANT_* table and FKC's mirror of it have diverged — update \
2120 FDX_FAMILY_TOKENS *and* is_fdx_quant_family together"
2121 );
2122
2123 let declared_grans: std::collections::BTreeSet<String> =
2124 FDX_GRAN_TOKENS.iter().map(|(c, _)| c.to_string()).collect();
2125 assert_eq!(
2126 grans, declared_grans,
2127 "FDX's FDX_SCALE_GRAN_* table and FKC's mirror of it have diverged"
2128 );
2129
2130 for (konst, token) in FDX_FAMILY_TOKENS {
2132 assert!(
2133 is_fdx_quant_family(token),
2134 "FKC rejects `{token}`, the token for the live FDX const {konst}"
2135 );
2136 }
2137 for (konst, token) in FDX_GRAN_TOKENS {
2138 assert!(
2139 is_fdx_granularity(token),
2140 "FKC rejects `{token}`, the token for the live FDX const {konst}"
2141 );
2142 }
2143
2144 assert!(is_fdx_quant_family("none"));
2146
2147 for bogus in ["PerGroup", "AWQ", "GPTQ", "affine_int", "PERTENSOR", ""] {
2150 assert!(!is_fdx_quant_family(bogus), "family accepted `{bogus}`");
2151 assert!(!is_fdx_granularity(bogus), "granularity accepted `{bogus}`");
2152 }
2153 }
2154
2155 #[test]
2157 fn missing_provenance_errors() {
2158 let src = valid_bundle().replace(" provenance: declared\n", "");
2159 let err = validate_str(&src).expect_err("no provenance");
2160 assert!(
2161 matches!(err, FkcError::CostProvenanceMissing { .. }),
2162 "got {err:?}"
2163 );
2164 }
2165
2166 #[test]
2167 fn bad_cost_expr_is_cost_expr_parse() {
2168 let src = valid_bundle().replace("flops: \"n\"", "flops: \"2 * * n\"");
2169 let err = validate_str(&src).expect_err("malformed flops");
2170 assert!(matches!(err, FkcError::CostExprParse { .. }), "got {err:?}");
2171 }
2172
2173 #[test]
2174 fn declared_with_no_coeffs_and_no_class_is_placeholder() {
2175 let src = valid_bundle()
2176 .replace(" class: cheap_elementwise\n", "")
2177 .replace(" flops: \"n\"\n", "")
2178 .replace(" bytes_moved: \"3 * n * 4\"\n", "");
2179 let err = validate_str(&src).expect_err("bare declared cost");
2180 assert!(
2181 matches!(err, FkcError::PlaceholderCost { .. }),
2182 "got {err:?}"
2183 );
2184 }
2185
2186 #[test]
2187 fn judge_measured_all_tilde_is_not_placeholder() {
2188 let src = valid_bundle()
2191 .replace("provenance: declared", "provenance: judge_measured")
2192 .replace(" flops: \"n\"\n", " flops: ~\n")
2193 .replace(" bytes_moved: \"3 * n * 4\"\n", " bytes_moved: ~\n");
2194 validate_str(&src).expect("judge_measured + ~ is fine");
2195 }
2196
2197 #[test]
2198 fn placeholder_cost_class_field() {
2199 use crate::fkc::cost_compile::{CostClassKind, classify_cost};
2200 assert!(classify_cost("declared", "cheap_elementwise", false, false).is_none());
2201 assert!(matches!(
2202 classify_cost("declared", "free", false, false),
2203 Some(CostClassKind::Free)
2204 ));
2205 assert!(matches!(
2206 classify_cost("declared", "gemm_like", true, false),
2207 Some(CostClassKind::DeclaredFormula)
2208 ));
2209 assert!(matches!(
2210 classify_cost("declared", "gemm_like", false, true),
2211 Some(CostClassKind::VendorSpec)
2212 ));
2213 assert!(matches!(
2214 classify_cost("judge_measured", "gemm_like", false, false),
2215 Some(CostClassKind::JudgeMeasured)
2216 ));
2217 use crate::fkc::register::import_bundle_str;
2219 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";
2220 let err = import_bundle_str(src, &StubLink::new())
2221 .expect_err("declared + non-free class + no usable cost path must reject");
2222 assert!(
2223 matches!(err, FkcError::PlaceholderCost { .. }),
2224 "got {err:?}"
2225 );
2226 }
2227
2228 #[test]
2230 fn nondeterministic_with_bitstable_true_is_incoherent() {
2231 let src = valid_bundle().replace(
2232 "determinism: same_hardware_bitwise",
2233 "determinism: nondeterministic",
2234 );
2235 let err = validate_str(&src).expect_err("nondeterministic + bit_stable true");
2236 assert!(
2237 matches!(err, FkcError::QuantIncoherent { .. }),
2238 "got {err:?}"
2239 );
2240 }
2241
2242 #[test]
2244 fn unknown_extent_kind_is_typed_error() {
2245 let src = quant_bundle("none", "").replace(
2246 " fdx:\n requires_ext: true\n quant:\n family: none\n",
2247 " fdx:\n extent_kind: wobbly\n symbolic_extent: required\n",
2248 );
2249 let err = validate_str(&src).expect_err("bad extent_kind");
2250 assert!(
2251 matches!(err, FkcError::UnknownAdmissibilityEnum { .. }),
2252 "got {err:?}"
2253 );
2254 }
2255
2256 fn describe_only_bundle() -> String {
2262 r#"---
2263fkc_version: 1
2264provider:
2265 name: test-provider
2266 backend: Cpu
2267 kernel_source: "test-cpu"
2268---
2269
2270# describe-only bundle
2271
2272## binary
2273
2274A chassis umbrella.
2275
2276```fkc
2277kernel: binary
2278registrable: false
2279op_kind: binary
2280blurb: "shared binary chassis (documentation umbrella)"
2281entry_point: "x::chassis"
2282accept:
2283 inputs:
2284 - name: lhs
2285 dtypes: [F32, F64, BF16, F16]
2286 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2287 - name: rhs
2288 dtypes: [F32, F64, BF16, F16]
2289 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2290return:
2291 outputs:
2292 - name: out
2293 dtype_rule: passthrough(lhs)
2294cost:
2295 provenance: judge_measured
2296 class: cheap_elementwise
2297precision:
2298 bit_stable_on_same_hardware: true
2299 audited: true
2300determinism: same_hardware_bitwise
2301```
2302"#
2303 .to_string()
2304 }
2305
2306 #[test]
2307 fn describe_only_with_descriptive_op_kind_validates() {
2308 validate_str(&describe_only_bundle()).expect("describe-only section validates");
2311 }
2312
2313 #[test]
2314 fn describe_only_with_tilde_op_kind_validates() {
2315 let src = describe_only_bundle().replace("op_kind: binary\n", "op_kind: ~\n");
2318 validate_str(&src).expect("describe-only with op_kind: ~ validates");
2319 }
2320
2321 #[test]
2322 fn non_describe_only_still_requires_real_op_kind_regression() {
2323 let src = describe_only_bundle().replace("registrable: false\n", "");
2327 let err = validate_str(&src).expect_err("registrable section needs a real op_kind");
2328 assert!(matches!(err, FkcError::UnknownOpKind { .. }), "got {err:?}");
2329 }
2330
2331 #[test]
2332 fn describe_only_still_validates_descriptive_dtypes() {
2333 let src = describe_only_bundle().replace("dtypes: [F32, F64, BF16, F16]", "dtypes: [F99]");
2337 let err = validate_str(&src).expect_err("bad dtype in describe-only docs");
2338 assert!(
2339 matches!(err, FkcError::FdxTokenNotInTable { .. }),
2340 "got {err:?}"
2341 );
2342 }
2343
2344 #[test]
2345 fn describe_only_still_validates_layout_coherence() {
2346 let src = describe_only_bundle().replace(
2348 "layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
2349 "layout: { contiguous: \"n/a\", strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }",
2350 );
2351 let err = validate_str(&src).expect_err("incoherent layout in describe-only docs");
2352 assert!(
2353 matches!(err, FkcError::LayoutIncoherent { .. }),
2354 "got {err:?}"
2355 );
2356 }
2357
2358 fn describe_only_zero_input_bundle() -> String {
2363 r#"---
2364fkc_version: 1
2365provider:
2366 name: test-provider
2367 backend: Cpu
2368 kernel_source: "test-cpu"
2369---
2370
2371# describe-only zero-input bundle
2372
2373## rand_fill
2374
2375A zero-operand random fill.
2376
2377```fkc
2378kernel: rand_fill
2379registrable: false
2380op_kind: RandUniform
2381blurb: "fill a dense buffer with U(min,max); no input tensor (RNG state is backend-private)"
2382entry_point: "x::rand_fill"
2383accept:
2384 inputs: []
2385return:
2386 outputs:
2387 - name: out
2388 dtype_rule: cast(out)
2389cost:
2390 provenance: judge_measured
2391 class: cheap_elementwise
2392precision:
2393 bit_stable_on_same_hardware: false
2394 audited: true
2395determinism: nondeterministic
2396```
2397"#
2398 .to_string()
2399 }
2400
2401 #[test]
2402 fn describe_only_zero_input_fill_validates() {
2403 validate_str(&describe_only_zero_input_bundle())
2407 .expect("describe-only zero-input fill validates");
2408 }
2409
2410 #[test]
2411 fn registrable_zero_input_still_fails_missing_required_field() {
2412 let src = describe_only_zero_input_bundle()
2417 .replace("registrable: false\n", "")
2418 .replace("op_kind: RandUniform\n", "op_kind: ReluElementwise\n");
2422 let err = validate_str(&src).expect_err("registrable zero-input section is rejected");
2423 assert!(
2424 matches!(err, FkcError::MissingRequiredField { ref field, .. } if field == "accept.inputs (≥1)"),
2425 "got {err:?}"
2426 );
2427 }
2428
2429 fn paged_blocks_bundle() -> String {
2440 r#"---
2441fkc_version: 1
2442provider:
2443 name: test-provider
2444 backend: Cpu
2445 kernel_source: "test-cpu"
2446---
2447
2448# paged bundle
2449
2450## paged_demo
2451
2452A paged attention blurb.
2453
2454```fkc
2455kernel: paged_demo
2456op_kind: PagedAttn
2457blurb: "naive paged attention over a blocked KV cache"
2458entry_point: "x::paged_demo"
2459accept:
2460 inputs:
2461 - name: q
2462 dtypes: [F32]
2463 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2464 - name: k_cache
2465 dtypes: [F32]
2466 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2467 fdx:
2468 requires_ext: true
2469 symbolic_extent: required
2470 gather: { kind: paged_blocks, block_table: block_table, context_lens: context_lens }
2471 - name: block_table
2472 dtypes: [U32]
2473 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2474 - name: context_lens
2475 dtypes: [U32]
2476 layout: { contiguous: required, strided: rejected, broadcast_stride0: rejected, start_offset: rejected, reverse_strides: rejected }
2477 op_params: { variant: PagedAttn }
2478return:
2479 outputs:
2480 - name: out
2481 dtype_rule: passthrough(q)
2482cost:
2483 provenance: declared
2484 class: attention
2485 flops: "4 * b * hq * sq * ctx * d"
2486 bytes_moved: "2 * b * hq * sq * d * 4"
2487precision:
2488 bit_stable_on_same_hardware: true
2489 audited: true
2490determinism: same_hardware_bitwise
2491```
2492"#
2493 .to_string()
2494 }
2495
2496 #[test]
2497 fn coherent_registrable_paged_blocks_validates() {
2498 validate_str(&paged_blocks_bundle())
2507 .expect("a coherent registrable paged_blocks section validates + is registrable");
2508 }
2509
2510 #[test]
2511 fn incoherent_gather_missing_requires_ext_still_errors() {
2512 let src = paged_blocks_bundle().replace(" requires_ext: true\n", "");
2516 let err = validate_str(&src).expect_err("gather without requires_ext must error");
2517 assert!(
2518 matches!(err, FkcError::GatherIncoherent { ref reason, .. } if reason.contains("requires_ext")),
2519 "got {err:?}",
2520 );
2521 }
2522
2523 #[test]
2524 fn incoherent_gather_dangling_block_table_role_still_errors() {
2525 let src = paged_blocks_bundle().replace("block_table: block_table,", "block_table: nope,");
2530 let err = validate_str(&src).expect_err("dangling block_table role must error");
2531 assert!(
2532 matches!(err, FkcError::GatherIncoherent { ref reason, .. } if reason.contains("block_table")),
2533 "got {err:?}",
2534 );
2535 }
2536
2537 #[test]
2538 fn unknown_gather_kind_is_unknown_admissibility_enum() {
2539 let src = paged_blocks_bundle().replace("kind: paged_blocks,", "kind: ragged_csr,");
2542 let err = validate_str(&src).expect_err("unknown gather kind must error");
2543 assert!(
2544 matches!(err, FkcError::UnknownAdmissibilityEnum { ref value, .. } if value == "ragged_csr"),
2545 "got {err:?}",
2546 );
2547 }
2548
2549 struct StubLink {
2557 seen: std::sync::Mutex<std::collections::HashMap<String, crate::kernel::KernelRef>>,
2558 }
2559 impl StubLink {
2560 fn new() -> Self {
2561 Self {
2562 seen: std::sync::Mutex::new(std::collections::HashMap::new()),
2563 }
2564 }
2565 fn resolve(&self, symbol: &str) -> Option<crate::kernel::KernelRef> {
2566 use std::sync::Arc;
2567 use std::sync::RwLock;
2568 fn k0(
2572 _i: &[Arc<RwLock<fuel_memory::Storage>>],
2573 _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2574 _l: &[fuel_ir::Layout],
2575 _p: &crate::kernel::OpParams,
2576 ) -> fuel_ir::Result<()> {
2577 Ok(())
2578 }
2579 fn k1(
2580 _i: &[Arc<RwLock<fuel_memory::Storage>>],
2581 _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2582 _l: &[fuel_ir::Layout],
2583 _p: &crate::kernel::OpParams,
2584 ) -> fuel_ir::Result<()> {
2585 Ok(())
2586 }
2587 fn k2(
2588 _i: &[Arc<RwLock<fuel_memory::Storage>>],
2589 _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2590 _l: &[fuel_ir::Layout],
2591 _p: &crate::kernel::OpParams,
2592 ) -> fuel_ir::Result<()> {
2593 Ok(())
2594 }
2595 fn k3(
2596 _i: &[Arc<RwLock<fuel_memory::Storage>>],
2597 _o: &mut [Arc<RwLock<fuel_memory::Storage>>],
2598 _l: &[fuel_ir::Layout],
2599 _p: &crate::kernel::OpParams,
2600 ) -> fuel_ir::Result<()> {
2601 Ok(())
2602 }
2603 let table: [crate::kernel::KernelRef; 4] = [k0, k1, k2, k3];
2604 let mut g = self.seen.lock().unwrap();
2605 if let Some(k) = g.get(symbol) {
2606 return Some(*k);
2607 }
2608 let k = table[g.len() % table.len()];
2609 g.insert(symbol.to_string(), k);
2610 Some(k)
2611 }
2612 }
2613 impl crate::fkc::lower::LinkRegistry for StubLink {
2614 fn resolve_primitive(&self, s: &str) -> Option<crate::kernel::KernelRef> {
2615 self.resolve(s)
2616 }
2617 fn resolve_fused(&self, s: &str) -> Option<crate::kernel::KernelRef> {
2618 self.resolve(s)
2619 }
2620 fn resolve_cost_fn(&self, _name: &str) -> Option<crate::kernel::CostFn> {
2621 fn c(
2627 _s: &[fuel_ir::Shape],
2628 _d: &[fuel_ir::DType],
2629 _p: &crate::kernel::OpParams,
2630 _c: &fuel_ir::backend::BackendCapabilities,
2631 ) -> crate::fused::CostEstimate {
2632 crate::fused::CostEstimate::default()
2633 }
2634 Some(c)
2635 }
2636 }
2637
2638 fn collect_fkc_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
2641 let rd = std::fs::read_dir(dir).unwrap_or_else(|e| {
2646 panic!(
2647 "collect_fkc_files: read_dir({}) failed: {e} — the FKC file sweep must FAIL, not silently truncate",
2648 dir.display()
2649 )
2650 });
2651 for entry in rd.flatten() {
2652 let path = entry.path();
2653 if path.is_dir() {
2654 if path.file_name().and_then(|n| n.to_str()) == Some("_inventory") {
2655 continue;
2656 }
2657 collect_fkc_files(&path, out);
2658 } else if path.to_string_lossy().ends_with(".fkc.md") {
2659 out.push(path);
2660 }
2661 }
2662 }
2663
2664 #[test]
2692 fn ci_lint_corpus_parse_lower_validate() {
2693 let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2696 let corpus = manifest.join("kernel-contracts");
2697 assert!(
2698 corpus.is_dir(),
2699 "corpus dir not found at {}",
2700 corpus.display()
2701 );
2702
2703 let mut files = Vec::new();
2704 collect_fkc_files(&corpus, &mut files);
2705 files.sort();
2706 assert!(!files.is_empty(), "no .fkc.md files found");
2707
2708 let link = StubLink::new();
2709 let mut file_count = 0usize;
2710 let mut section_count = 0usize;
2711 let mut hard_failures: Vec<String> = Vec::new();
2712 let mut deferred: Vec<String> = Vec::new();
2713
2714 for path in &files {
2715 file_count += 1;
2716 let rel = path
2717 .strip_prefix(manifest)
2718 .unwrap_or(path)
2719 .display()
2720 .to_string();
2721 let src = std::fs::read_to_string(path).expect("read corpus file");
2722
2723 let file = match parse_file(&src) {
2725 Ok(f) => f,
2726 Err(e) => {
2727 hard_failures.push(format!("{rel}: PARSE: {e}"));
2728 continue;
2729 }
2730 };
2731 section_count += file.kernels.len();
2732
2733 if !fkc_version_is_supported(file.front_matter.fkc_version) {
2737 hard_failures.push(format!(
2738 "{rel}: fkc_version {} not in supported set {:?}",
2739 file.front_matter.fkc_version, FKC_SUPPORTED_VERSIONS
2740 ));
2741 continue;
2742 }
2743 for kernel in &file.kernels {
2744 let kname = kernel.kernel.as_str();
2745 match validate_kernel(kernel) {
2746 Ok(()) => {}
2747 Err(
2748 e @ (FkcError::MxNotYetRegistrable { .. }
2749 | FkcError::GatherNotYetSupported { .. }),
2750 ) => {
2751 deferred.push(format!("{rel} :: {kname}: {e}"));
2752 }
2753 Err(e) => {
2754 hard_failures.push(format!("{rel} :: {kname}: {e}"));
2755 continue;
2756 }
2757 }
2758 if let Err(e) = lower_one_kernel(&file, kernel, &link) {
2762 match e {
2766 FkcError::MxNotYetRegistrable { .. }
2767 | FkcError::GatherNotYetSupported { .. } => {}
2768 FkcError::FanoutDtypeMismatch { .. } => {
2779 deferred.push(format!("{rel} :: {kname}: LOWER: {e}"));
2780 }
2781 FkcError::OptionalOperandNotLast { .. } => {
2792 deferred.push(format!("{rel} :: {kname}: LOWER: {e}"));
2793 }
2794 _ => hard_failures.push(format!("{rel} :: {kname}: LOWER: {e}")),
2795 }
2796 }
2797 }
2798 }
2799
2800 eprintln!(
2801 "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",
2802 deferred.len(),
2803 hard_failures.len()
2804 );
2805 if !deferred.is_empty() {
2806 eprintln!(
2807 "--- deferred (describable, not-yet-registrable / not-yet-fannable; spec §6/§3.9.1/§3.4) ---"
2808 );
2809 for d in &deferred {
2810 eprintln!(" {d}");
2811 }
2812 }
2813 if !hard_failures.is_empty() {
2814 eprintln!("--- HARD FAILURES (file :: kernel: rule) ---");
2815 for f in &hard_failures {
2816 eprintln!(" {f}");
2817 }
2818 panic!(
2819 "{} corpus contract(s) failed parse/lower/validate — see the list above \
2820 (REPORT these, do not relax the validators)",
2821 hard_failures.len()
2822 );
2823 }
2824 }
2825
2826 #[test]
2850 fn ci_lint_corpus_has_no_cross_file_entry_point_collisions() {
2851 let manifest = std::path::Path::new(env!("CARGO_MANIFEST_DIR"));
2852 let corpus = manifest.join("kernel-contracts");
2853 assert!(
2854 corpus.is_dir(),
2855 "corpus dir not found at {}",
2856 corpus.display()
2857 );
2858
2859 let mut files = Vec::new();
2860 collect_fkc_files(&corpus, &mut files);
2861 files.sort();
2862 assert!(!files.is_empty(), "no .fkc.md files found");
2863
2864 let mut claimed: std::collections::HashMap<(String, String), String> =
2866 std::collections::HashMap::new();
2867 let mut collisions: Vec<(String, String, String)> = Vec::new();
2869 let mut checked = 0usize;
2870
2871 for path in &files {
2872 let rel = path
2873 .strip_prefix(manifest)
2874 .unwrap_or(path)
2875 .display()
2876 .to_string();
2877 let src = std::fs::read_to_string(path).expect("read corpus file");
2878 let Ok(file) = parse_file(&src) else {
2879 continue;
2882 };
2883 let kernel_source = file.front_matter.provider.kernel_source.clone();
2884 for kernel in &file.kernels {
2885 if !kernel.registrable {
2888 continue;
2889 }
2890 let Some(entry) = kernel.entry_point.as_ref() else {
2891 continue;
2892 };
2893 checked += 1;
2894 let key = (kernel_source.clone(), entry.clone());
2895 let who = format!("{rel} :: {}", kernel.kernel);
2896 match claimed.get(&key) {
2897 Some(first) => collisions.push((
2898 key.0.clone(),
2899 key.1.clone(),
2900 format!("claimed by BOTH {first} AND {who}"),
2901 )),
2902 None => {
2903 claimed.insert(key, who);
2904 }
2905 }
2906 }
2907 }
2908
2909 eprintln!(
2912 "FKC corpus entry-point uniqueness: {checked} registrable contracts across {} files, {} distinct (kernel_source, entry_point) keys",
2913 files.len(),
2914 claimed.len()
2915 );
2916 assert!(
2917 checked > 0,
2918 "the lint examined zero registrable contracts — it is not looking where it thinks"
2919 );
2920
2921 const KNOWN: &[(&str, &str)] = &[
2938 (
2940 "portable-cpu",
2941 "fuel_cpu_backend::byte_kernels::sum_reduce_f32",
2942 ),
2943 (
2944 "portable-cpu",
2945 "fuel_cpu_backend::byte_kernels::max_reduce_f32",
2946 ),
2947 (
2948 "portable-cpu",
2949 "fuel_cpu_backend::byte_kernels::min_reduce_f32",
2950 ),
2951 (
2952 "portable-cpu",
2953 "fuel_cpu_backend::byte_kernels::mean_reduce_f32",
2954 ),
2955 (
2956 "portable-cpu",
2957 "fuel_cpu_backend::byte_kernels::reduce_sum_to_f32",
2958 ),
2959 (
2960 "portable-cpu",
2961 "fuel_cpu_backend::byte_kernels::reduce_max_to_f32",
2962 ),
2963 (
2964 "portable-cpu",
2965 "fuel_cpu_backend::byte_kernels::reduce_max_to_backward_f32",
2966 ),
2967 ("vulkan-slang", "fuel_vulkan_backend::fkc::rope_f32"),
2972 ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_f32"),
2974 ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_bf16"),
2975 ("vulkan-slang", "fuel_vulkan_backend::fkc::flash_attn_f16"),
2976 (
2977 "vulkan-slang",
2978 "fuel_vulkan_backend::fkc::flash_attn_backward_q_f32",
2979 ),
2980 (
2981 "vulkan-slang",
2982 "fuel_vulkan_backend::fkc::flash_attn_backward_k_f32",
2983 ),
2984 (
2985 "vulkan-slang",
2986 "fuel_vulkan_backend::fkc::flash_attn_backward_v_f32",
2987 ),
2988 ];
2989 let is_known =
2990 |key: &(String, String)| KNOWN.iter().any(|(ks, ep)| *ks == key.0 && *ep == key.1);
2991
2992 let novel: Vec<&(String, String, String)> = collisions
2993 .iter()
2994 .filter(|c| !is_known(&(c.0.clone(), c.1.clone())))
2995 .collect();
2996
2997 let seen: std::collections::HashSet<(String, String)> = collisions
3001 .iter()
3002 .map(|c| (c.0.clone(), c.1.clone()))
3003 .collect();
3004 let stale: Vec<&(&str, &str)> = KNOWN
3005 .iter()
3006 .filter(|(ks, ep)| !seen.contains(&((*ks).to_string(), (*ep).to_string())))
3007 .collect();
3008
3009 if !stale.is_empty() {
3010 for (ks, ep) in &stale {
3011 eprintln!(" STALE ALLOWLIST ENTRY (no longer collides): {ks} :: {ep}");
3012 }
3013 panic!(
3014 "{} allowlisted collision(s) no longer occur — delete them from KNOWN so the list keeps reflecting the real debt",
3015 stale.len()
3016 );
3017 }
3018
3019 if !novel.is_empty() {
3020 for (ks, ep, who) in &novel {
3021 eprintln!(" NEW COLLISION: kernel_source `{ks}` entry_point `{ep}` — {who}");
3022 }
3023 panic!(
3024 "{} 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.",
3025 novel.len()
3026 );
3027 }
3028 }
3029
3030 fn lower_one_kernel(
3035 file: &FkcFile,
3036 kernel: &FkcKernel,
3037 link: &dyn crate::fkc::lower::LinkRegistry,
3038 ) -> Result<(), FkcError> {
3039 let mut one = file.clone();
3042 one.kernels = vec![kernel.clone()];
3043 crate::fkc::lower::lower_file(&one, link, &mut Vec::new()).map(|_| ())
3044 }
3045}