Skip to main content

vyre_lower/verify/
mod.rs

1//! Descriptor invariant verifier.
2//!
3//! Walks a `KernelDescriptor` checking structural invariants that
4//! every well-formed descriptor must satisfy:
5//!
6//! 1. Within each `KernelBody`, `op.result` ids are unique.
7//! 2. Operand positions classified as result-id references must
8//!    point at a result-id produced in the same body or lexically
9//!    captured from a parent structured-control body.
10//! 3. Operand positions classified as literal-pool indices must be in
11//!    range of the body's `literals` vector.
12//! 4. Operand positions classified as child-body indices must be in
13//!    range of the body's `child_bodies` vector.
14//! 5. Every `KernelOpKind::Literal` op must have at least one operand
15//!    (the pool index).
16//!
17//! Bodies recurse with lexical scope and loop-carried visibility.
18//! `vyre-lower` allocates result ids globally for the descriptor:
19//! structured child bodies may reference values available before the
20//! child was attached, and parent bodies may reference values assigned
21//! by a completed child body.
22//!
23//! ## Wiring
24//!
25//! Useful as a debug-time check after every rewrite pass. Not yet
26//! wired into `run_all` because the established invariant is "every
27//! rewrite preserves verify()"  -  wire when the user asks. Direct
28//! callers (rewrite tests, fuzz harnesses) can call `verify()` to
29//! turn quiet bugs into loud ones.
30
31use rustc_hash::FxHashSet;
32use serde::{Deserialize, Serialize};
33
34use crate::{KernelBody, KernelDescriptor, KernelOpKind};
35
36/// Result type  -  `Ok(())` if every invariant holds; `Err(Vec)` lists
37/// every violation found, not just the first.
38pub type VerifyResult = Result<(), Vec<VerifyError>>;
39
40#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
41pub struct VerifyError {
42    pub body_path: Vec<usize>,
43    pub op_index: usize,
44    pub kind: VerifyErrorKind,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
48pub enum VerifyErrorKind {
49    DuplicateResultId(u32),
50    DanglingResultRef {
51        operand_pos: usize,
52        ref_id: u32,
53    },
54    LiteralPoolOutOfRange {
55        operand_pos: usize,
56        pool_idx: u32,
57        pool_size: usize,
58    },
59    ChildBodyIndexOutOfRange {
60        operand_pos: usize,
61        body_idx: u32,
62        child_count: usize,
63    },
64    LiteralOpMissingPoolOperand,
65    OperandCountTooShort {
66        expected_min: usize,
67        got: usize,
68    },
69    /// `dispatch.workgroup_size[axis]` is zero. A kernel with a zero
70    /// dim never runs  -  almost certainly a host-side bug.
71    DispatchZeroDim {
72        axis: u8,
73    },
74    /// Two `BindingSlot` entries share the same `.slot` field. The
75    /// emitters look up bindings by `.slot`; duplicates make the
76    /// lookup ambiguous.
77    DuplicateBindingSlotId {
78        slot: u32,
79    },
80    /// A host-bound binding (`Global` / `Constant` / `Uniform`) sits
81    /// in the workgroup-reserved slot range (`>= 1<<24`). Backend
82    /// bind-group layouts cap at 1000 bindings on wgpu and similar
83    /// limits elsewhere; a host slot in the reserved range fails
84    /// layout creation with a "binding index N greater than maximum"
85    /// validator error. Earlier rewrites should have allocated the
86    /// new slot in the host range.
87    HostBindingInWorkgroupRange {
88        slot: u32,
89    },
90    /// A workgroup binding (`Shared` / `Scratch`) sits in the
91    /// host-bindable slot range (`< 1<<24`). The host dispatch path
92    /// addresses host bindings by slot id; a workgroup binding in
93    /// that range can collide with a Global binding's slot id and
94    /// silently steer load/store ops to the wrong memory class.
95    WorkgroupBindingInHostRange {
96        slot: u32,
97    },
98}
99
100#[must_use]
101pub fn verify(desc: &KernelDescriptor) -> VerifyResult {
102    use rustc_hash::FxHashSet;
103    let mut errors = Vec::new();
104    // Dispatch-level checks (don't have a body_path).
105    for (axis, &dim) in desc.dispatch.workgroup_size.iter().enumerate() {
106        if dim == 0 {
107            errors.push(VerifyError {
108                body_path: vec![],
109                op_index: 0,
110                kind: VerifyErrorKind::DispatchZeroDim { axis: axis as u8 },
111            });
112        }
113    }
114    // Binding-layout checks: no two slots share `.slot` field; host vs
115    // workgroup ranges stay segregated.
116    use crate::descriptor::MemoryClass;
117    let mut seen_slots: FxHashSet<u32> = FxHashSet::default();
118    for s in &desc.bindings.slots {
119        if !seen_slots.insert(s.slot) {
120            errors.push(VerifyError {
121                body_path: vec![],
122                op_index: 0,
123                kind: VerifyErrorKind::DuplicateBindingSlotId { slot: s.slot },
124            });
125        }
126        let in_workgroup_range = s.slot >= crate::lower::WORKGROUP_SLOT_BASE;
127        let is_workgroup_class =
128            matches!(s.memory_class, MemoryClass::Shared | MemoryClass::Scratch,);
129        if in_workgroup_range && !is_workgroup_class {
130            errors.push(VerifyError {
131                body_path: vec![],
132                op_index: 0,
133                kind: VerifyErrorKind::HostBindingInWorkgroupRange { slot: s.slot },
134            });
135        }
136        if !in_workgroup_range && is_workgroup_class {
137            errors.push(VerifyError {
138                body_path: vec![],
139                op_index: 0,
140                kind: VerifyErrorKind::WorkgroupBindingInHostRange { slot: s.slot },
141            });
142        }
143    }
144    verify_body(
145        &desc.body,
146        &mut Vec::new(),
147        &FxHashSet::default(),
148        &mut errors,
149    );
150    if errors.is_empty() {
151        Ok(())
152    } else {
153        Err(errors)
154    }
155}
156
157fn verify_body(
158    body: &KernelBody,
159    path: &mut Vec<usize>,
160    inherited_results: &FxHashSet<u32>,
161    errors: &mut Vec<VerifyError>,
162) {
163    use rustc_hash::FxHashSet;
164
165    // 1. Collect produced result-ids, flagging duplicates.
166    let mut produced: FxHashSet<u32> = FxHashSet::default();
167    for (i, op) in body.ops.iter().enumerate() {
168        for r in op.result_ids() {
169            if !produced.insert(r) {
170                errors.push(VerifyError {
171                    body_path: path.clone(),
172                    op_index: i,
173                    kind: VerifyErrorKind::DuplicateResultId(r),
174                });
175            }
176        }
177    }
178
179    // 2 & 3 & 4 & 5: per-op operand checks.
180    let mut produced_so_far: FxHashSet<u32> = FxHashSet::default();
181    let child_results: Vec<FxHashSet<u32>> =
182        body.child_bodies.iter().map(collect_body_results).collect();
183    let mut completed_child_results: FxHashSet<u32> = FxHashSet::default();
184    let mut child_scopes = vec![FxHashSet::default(); body.child_bodies.len()];
185    for (i, op) in body.ops.iter().enumerate() {
186        // Literal ops must have at least one operand (pool index).
187        if matches!(op.kind, KernelOpKind::Literal) {
188            if op.operands.is_empty() {
189                errors.push(VerifyError {
190                    body_path: path.clone(),
191                    op_index: i,
192                    kind: VerifyErrorKind::LiteralOpMissingPoolOperand,
193                });
194            } else {
195                let pool_idx = op.operands[0];
196                if (pool_idx as usize) >= body.literals.len() {
197                    errors.push(VerifyError {
198                        body_path: path.clone(),
199                        op_index: i,
200                        kind: VerifyErrorKind::LiteralPoolOutOfRange {
201                            operand_pos: 0,
202                            pool_idx,
203                            pool_size: body.literals.len(),
204                        },
205                    });
206                }
207            }
208        }
209
210        // Per-position classification.
211        for (pos, &val) in op.operands.iter().enumerate() {
212            let cls = classify_operand(&op.kind, pos);
213            match cls {
214                OperandClass::ResultRef => {
215                    if !produced_so_far.contains(&val)
216                        && !produced.contains(&val)
217                        && !inherited_results.contains(&val)
218                        && !completed_child_results.contains(&val)
219                    {
220                        errors.push(VerifyError {
221                            body_path: path.clone(),
222                            op_index: i,
223                            kind: VerifyErrorKind::DanglingResultRef {
224                                operand_pos: pos,
225                                ref_id: val,
226                            },
227                        });
228                    }
229                }
230                OperandClass::ChildBodyIdx => {
231                    if (val as usize) >= body.child_bodies.len() {
232                        errors.push(VerifyError {
233                            body_path: path.clone(),
234                            op_index: i,
235                            kind: VerifyErrorKind::ChildBodyIndexOutOfRange {
236                                operand_pos: pos,
237                                body_idx: val,
238                                child_count: body.child_bodies.len(),
239                            },
240                        });
241                    } else {
242                        let child_scope = &mut child_scopes[val as usize];
243                        child_scope.extend(inherited_results.iter().copied());
244                        child_scope.extend(produced_so_far.iter().copied());
245                        child_scope.extend(completed_child_results.iter().copied());
246                    }
247                }
248                OperandClass::LiteralPoolIdx => {
249                    if (val as usize) >= body.literals.len() {
250                        errors.push(VerifyError {
251                            body_path: path.clone(),
252                            op_index: i,
253                            kind: VerifyErrorKind::LiteralPoolOutOfRange {
254                                operand_pos: pos,
255                                pool_idx: val,
256                                pool_size: body.literals.len(),
257                            },
258                        });
259                    }
260                }
261                OperandClass::Other => {}
262            }
263        }
264
265        // Minimum operand count per kind. Conservative  -  we just check
266        // shapes the rewrites actually produce.
267        let min_required = min_operand_count(&op.kind);
268        if op.operands.len() < min_required {
269            errors.push(VerifyError {
270                body_path: path.clone(),
271                op_index: i,
272                kind: VerifyErrorKind::OperandCountTooShort {
273                    expected_min: min_required,
274                    got: op.operands.len(),
275                },
276            });
277        }
278
279        for r in op.result_ids() {
280            produced_so_far.insert(r);
281        }
282        for child_idx in child_body_operands(op) {
283            if let Some(results) = child_results.get(child_idx as usize) {
284                completed_child_results.extend(results.iter().copied());
285            }
286        }
287    }
288
289    // Recurse.
290    for (idx, child) in body.child_bodies.iter().enumerate() {
291        path.push(idx);
292        verify_body(child, path, &child_scopes[idx], errors);
293        path.pop();
294    }
295}
296
297fn collect_body_results(body: &KernelBody) -> FxHashSet<u32> {
298    let mut results = FxHashSet::default();
299    for op in &body.ops {
300        for result in op.result_ids() {
301            results.insert(result);
302        }
303    }
304    for child in &body.child_bodies {
305        results.extend(collect_body_results(child));
306    }
307    results
308}
309
310fn child_body_operands(op: &crate::KernelOp) -> impl Iterator<Item = u32> + '_ {
311    op.operands
312        .iter()
313        .enumerate()
314        .filter_map(|(pos, &operand)| {
315            (classify_operand(&op.kind, pos) == OperandClass::ChildBodyIdx).then_some(operand)
316        })
317}
318
319#[derive(Debug, Clone, Copy, PartialEq, Eq)]
320pub enum OperandClass {
321    ResultRef,
322    ChildBodyIdx,
323    LiteralPoolIdx,
324    /// Binding-slot literal, opaque tag, etc.  -  not validated structurally.
325    Other,
326}
327
328pub fn classify_operand(kind: &KernelOpKind, pos: usize) -> OperandClass {
329    use KernelOpKind::*;
330    match kind {
331        Literal => {
332            if pos == 0 {
333                OperandClass::LiteralPoolIdx
334            } else {
335                OperandClass::Other
336            }
337        }
338        LocalInvocationId | GlobalInvocationId | WorkgroupId => OperandClass::Other,
339        SubgroupLocalId | SubgroupSize => OperandClass::Other,
340        LoopIndex { .. } => OperandClass::Other,
341        BufferLength => OperandClass::Other,
342        LoadGlobal | LoadShared | LoadConstant => {
343            if pos == 0 {
344                OperandClass::Other
345            } else {
346                OperandClass::ResultRef
347            }
348        }
349        StoreGlobal | StoreShared => {
350            if pos == 0 {
351                OperandClass::Other
352            } else {
353                OperandClass::ResultRef
354            }
355        }
356        Copy | BinOpKind(_) | UnOpKind(_) | Fma | MatrixMma { .. } | Select | Cast { .. } => {
357            OperandClass::ResultRef
358        }
359        Atomic { .. } => {
360            if pos == 0 {
361                OperandClass::Other
362            } else {
363                OperandClass::ResultRef
364            }
365        }
366        SubgroupBallot | SubgroupShuffle | SubgroupBroadcast | SubgroupReduce { .. } => OperandClass::ResultRef,
367        StructuredIfThen => {
368            if pos == 0 {
369                OperandClass::ResultRef
370            } else if pos == 1 {
371                OperandClass::ChildBodyIdx
372            } else {
373                OperandClass::Other
374            }
375        }
376        StructuredIfThenElse => {
377            if pos == 0 {
378                OperandClass::ResultRef
379            } else if pos == 1 || pos == 2 {
380                OperandClass::ChildBodyIdx
381            } else {
382                OperandClass::Other
383            }
384        }
385        StructuredForLoop { .. } => {
386            if pos == 0 || pos == 1 {
387                OperandClass::ResultRef
388            } else if pos == 2 {
389                OperandClass::ChildBodyIdx
390            } else {
391                OperandClass::Other
392            }
393        }
394        StructuredBlock => {
395            if pos == 0 {
396                OperandClass::ChildBodyIdx
397            } else {
398                OperandClass::Other
399            }
400        }
401        Region { .. } => {
402            if pos == 0 {
403                OperandClass::ChildBodyIdx
404            } else {
405                OperandClass::Other
406            }
407        }
408        Return | Barrier { .. } => OperandClass::Other,
409        AsyncLoad { .. } | AsyncStore { .. } => {
410            if pos < 2 {
411                OperandClass::Other
412            } else {
413                OperandClass::ResultRef
414            }
415        }
416        AsyncWait { .. } => OperandClass::Other,
417        Trap { .. } => {
418            if pos == 0 {
419                OperandClass::ResultRef
420            } else {
421                OperandClass::Other
422            }
423        }
424        Resume { .. } => OperandClass::Other,
425        IndirectDispatch { .. } => OperandClass::Other,
426        Call { .. } => OperandClass::ResultRef,
427        OpaqueExpr(..) | OpaqueNode(..) => OperandClass::ResultRef,
428        LoopCarrierInit { .. } | LoopCarrier { .. } | LoopCarrierEnd { .. } => {
429            OperandClass::ResultRef
430        }
431    }
432}
433
434fn min_operand_count(kind: &KernelOpKind) -> usize {
435    use KernelOpKind::*;
436    match kind {
437        Literal => 1,
438        Copy => 1,
439        LocalInvocationId | GlobalInvocationId | WorkgroupId => 0,
440        SubgroupLocalId | SubgroupSize => 0,
441        LoopIndex { .. } => 0,
442        BufferLength => 1,
443        LoadGlobal | LoadShared | LoadConstant => 2,
444        StoreGlobal | StoreShared => 3,
445        BinOpKind(_) => 2,
446        UnOpKind(_) | Cast { .. } => 1,
447        Fma => 3,
448        MatrixMma { .. } => 10,
449        Select => 3,
450        Atomic { .. } => 2,
451        SubgroupBallot | SubgroupShuffle | SubgroupBroadcast | SubgroupReduce { .. } => 1,
452        StructuredIfThen => 2,
453        StructuredIfThenElse => 3,
454        StructuredForLoop { .. } => 3,
455        StructuredBlock => 1,
456        Region { .. } => 1,
457        Return => 0,
458        Barrier { .. } => 0,
459        AsyncLoad { .. } | AsyncStore { .. } => 2,
460        AsyncWait { .. } => 0,
461        Trap { .. } => 1,
462        Resume { .. } => 0,
463        IndirectDispatch { .. } => 0,
464        Call { .. } => 0,
465        OpaqueExpr(..) | OpaqueNode(..) => 0,
466        LoopCarrier { .. } => 0,
467        LoopCarrierInit { .. } | LoopCarrierEnd { .. } => 1,
468    }
469}
470
471#[cfg(test)]
472
473mod tests {
474    use super::*;
475    use crate::{
476        BindingLayout, Dispatch, KernelBody, KernelDescriptor, KernelOp, KernelOpKind, LiteralValue,
477    };
478    use vyre_foundation::ir::BinOp;
479
480    fn empty_desc(ops: Vec<KernelOp>, literals: Vec<LiteralValue>) -> KernelDescriptor {
481        KernelDescriptor {
482            id: "k".into(),
483            bindings: BindingLayout { slots: vec![] },
484            dispatch: Dispatch::new(1, 1, 1),
485            body: KernelBody {
486                ops,
487                child_bodies: vec![],
488                literals,
489            },
490        }
491    }
492
493    #[test]
494    fn empty_kernel_verifies() {
495        assert!(matches!(verify(&empty_desc(vec![], vec![])), Ok(_)));
496    }
497
498    #[test]
499    fn well_formed_kernel_verifies() {
500        let desc = empty_desc(
501            vec![
502                KernelOp {
503                    kind: KernelOpKind::Literal,
504                    operands: vec![0],
505                    result: Some(0),
506                },
507                KernelOp {
508                    kind: KernelOpKind::Literal,
509                    operands: vec![1],
510                    result: Some(1),
511                },
512                KernelOp {
513                    kind: KernelOpKind::BinOpKind(BinOp::Add),
514                    operands: vec![0, 1],
515                    result: Some(2),
516                },
517            ],
518            vec![LiteralValue::U32(3), LiteralValue::U32(4)],
519        );
520        assert_eq!(verify(&desc), Ok(()));
521    }
522
523    #[test]
524    fn duplicate_result_id_detected() {
525        let desc = empty_desc(
526            vec![
527                KernelOp {
528                    kind: KernelOpKind::Literal,
529                    operands: vec![0],
530                    result: Some(0),
531                },
532                KernelOp {
533                    kind: KernelOpKind::Literal,
534                    operands: vec![0],
535                    result: Some(0),
536                }, // dup
537            ],
538            vec![LiteralValue::U32(1)],
539        );
540        let r = verify(&desc);
541        assert!(r.is_err());
542        let errs = r.unwrap_err();
543        assert!(errs
544            .iter()
545            .any(|e| matches!(e.kind, VerifyErrorKind::DuplicateResultId(0))));
546    }
547
548    #[test]
549    fn dangling_result_ref_detected() {
550        let desc = empty_desc(
551            vec![
552                KernelOp {
553                    kind: KernelOpKind::Literal,
554                    operands: vec![0],
555                    result: Some(0),
556                },
557                KernelOp {
558                    kind: KernelOpKind::BinOpKind(BinOp::Add),
559                    operands: vec![0, 99], // 99 is not produced anywhere
560                    result: Some(1),
561                },
562            ],
563            vec![LiteralValue::U32(1)],
564        );
565        let r = verify(&desc);
566        let errs = r.unwrap_err();
567        assert!(errs.iter().any(|e| matches!(
568            e.kind,
569            VerifyErrorKind::DanglingResultRef { ref_id: 99, .. }
570        )));
571    }
572
573    #[test]
574    fn literal_pool_out_of_range_detected() {
575        let desc = empty_desc(
576            vec![KernelOp {
577                kind: KernelOpKind::Literal,
578                operands: vec![5], // pool only has 1 entry
579                result: Some(0),
580            }],
581            vec![LiteralValue::U32(1)],
582        );
583        let r = verify(&desc);
584        let errs = r.unwrap_err();
585        assert!(errs.iter().any(|e| matches!(
586            e.kind,
587            VerifyErrorKind::LiteralPoolOutOfRange {
588                pool_idx: 5,
589                pool_size: 1,
590                ..
591            }
592        )));
593    }
594
595    #[test]
596    fn child_body_index_out_of_range_detected() {
597        let desc = KernelDescriptor {
598            id: "k".into(),
599            bindings: BindingLayout { slots: vec![] },
600            dispatch: Dispatch::new(1, 1, 1),
601            body: KernelBody {
602                ops: vec![
603                    KernelOp {
604                        kind: KernelOpKind::Literal,
605                        operands: vec![0],
606                        result: Some(0),
607                    },
608                    KernelOp {
609                        kind: KernelOpKind::StructuredIfThen,
610                        operands: vec![0, 7], // child idx 7 with no children
611                        result: None,
612                    },
613                ],
614                child_bodies: vec![],
615                literals: vec![LiteralValue::U32(1)],
616            },
617        };
618        let r = verify(&desc);
619        let errs = r.unwrap_err();
620        assert!(errs.iter().any(|e| matches!(
621            e.kind,
622            VerifyErrorKind::ChildBodyIndexOutOfRange {
623                body_idx: 7,
624                child_count: 0,
625                ..
626            }
627        )));
628    }
629
630    #[test]
631    fn literal_op_with_no_operands_detected() {
632        let desc = empty_desc(
633            vec![KernelOp {
634                kind: KernelOpKind::Literal,
635                operands: vec![],
636                result: Some(0),
637            }],
638            vec![LiteralValue::U32(1)],
639        );
640        let r = verify(&desc);
641        let errs = r.unwrap_err();
642        assert!(errs
643            .iter()
644            .any(|e| matches!(e.kind, VerifyErrorKind::LiteralOpMissingPoolOperand)));
645    }
646
647    #[test]
648    fn operand_count_too_short_detected() {
649        let desc = empty_desc(
650            vec![
651                KernelOp {
652                    kind: KernelOpKind::Literal,
653                    operands: vec![0],
654                    result: Some(0),
655                },
656                KernelOp {
657                    kind: KernelOpKind::BinOpKind(BinOp::Add),
658                    operands: vec![0], // only 1 operand, Add needs 2
659                    result: Some(1),
660                },
661            ],
662            vec![LiteralValue::U32(1)],
663        );
664        let r = verify(&desc);
665        let errs = r.unwrap_err();
666        assert!(errs.iter().any(|e| matches!(
667            e.kind,
668            VerifyErrorKind::OperandCountTooShort {
669                expected_min: 2,
670                got: 1
671            }
672        )));
673    }
674
675    #[test]
676    fn errors_are_collected_not_short_circuited() {
677        // 3 distinct violations in one body.
678        let desc = empty_desc(
679            vec![
680                KernelOp {
681                    kind: KernelOpKind::Literal,
682                    operands: vec![99],
683                    result: Some(0),
684                }, // pool oor
685                KernelOp {
686                    kind: KernelOpKind::Literal,
687                    operands: vec![0],
688                    result: Some(0),
689                }, // dup
690                KernelOp {
691                    kind: KernelOpKind::BinOpKind(BinOp::Add),
692                    operands: vec![100, 200], // dangling refs
693                    result: Some(1),
694                },
695            ],
696            vec![LiteralValue::U32(1)],
697        );
698        let r = verify(&desc);
699        let errs = r.unwrap_err();
700        assert!(errs.len() >= 3);
701    }
702
703    #[test]
704    fn child_body_violations_recurse() {
705        let desc = KernelDescriptor {
706            id: "k".into(),
707            bindings: BindingLayout { slots: vec![] },
708            dispatch: Dispatch::new(1, 1, 1),
709            body: KernelBody {
710                ops: vec![],
711                child_bodies: vec![KernelBody {
712                    ops: vec![KernelOp {
713                        kind: KernelOpKind::Literal,
714                        operands: vec![99],
715                        result: Some(0),
716                    }],
717                    child_bodies: vec![],
718                    literals: vec![LiteralValue::U32(1)],
719                }],
720                literals: vec![],
721            },
722        };
723        let r = verify(&desc);
724        let errs = r.unwrap_err();
725        assert!(errs.iter().any(|e| e.body_path == vec![0]));
726    }
727
728    #[test]
729    fn child_body_may_capture_parent_result_available_before_control_op() {
730        let child = KernelBody {
731            ops: vec![KernelOp {
732                kind: KernelOpKind::BinOpKind(BinOp::Add),
733                operands: vec![0, 0],
734                result: Some(1),
735            }],
736            child_bodies: vec![],
737            literals: vec![],
738        };
739        let desc = KernelDescriptor {
740            id: "captures".into(),
741            bindings: BindingLayout { slots: vec![] },
742            dispatch: Dispatch::new(1, 1, 1),
743            body: KernelBody {
744                ops: vec![
745                    KernelOp {
746                        kind: KernelOpKind::Literal,
747                        operands: vec![0],
748                        result: Some(0),
749                    },
750                    KernelOp {
751                        kind: KernelOpKind::StructuredBlock,
752                        operands: vec![0],
753                        result: None,
754                    },
755                ],
756                child_bodies: vec![child],
757                literals: vec![LiteralValue::U32(7)],
758            },
759        };
760
761        assert_eq!(verify(&desc), Ok(()));
762    }
763
764    #[test]
765    fn child_body_cannot_capture_parent_result_declared_after_control_op() {
766        let child = KernelBody {
767            ops: vec![KernelOp {
768                kind: KernelOpKind::BinOpKind(BinOp::Add),
769                operands: vec![1, 1],
770                result: Some(2),
771            }],
772            child_bodies: vec![],
773            literals: vec![],
774        };
775        let desc = KernelDescriptor {
776            id: "future_capture".into(),
777            bindings: BindingLayout { slots: vec![] },
778            dispatch: Dispatch::new(1, 1, 1),
779            body: KernelBody {
780                ops: vec![
781                    KernelOp {
782                        kind: KernelOpKind::Literal,
783                        operands: vec![0],
784                        result: Some(0),
785                    },
786                    KernelOp {
787                        kind: KernelOpKind::StructuredBlock,
788                        operands: vec![0],
789                        result: None,
790                    },
791                    KernelOp {
792                        kind: KernelOpKind::Literal,
793                        operands: vec![1],
794                        result: Some(1),
795                    },
796                ],
797                child_bodies: vec![child],
798                literals: vec![LiteralValue::U32(7), LiteralValue::U32(9)],
799            },
800        };
801
802        let errors = verify(&desc).expect_err("future child capture must fail");
803        assert!(errors.iter().any(|error| {
804            error.body_path == vec![0]
805                && matches!(
806                    error.kind,
807                    VerifyErrorKind::DanglingResultRef { ref_id: 1, .. }
808                )
809        }));
810    }
811
812    #[test]
813    fn parent_body_may_read_result_assigned_by_completed_child_body() {
814        let child = KernelBody {
815            ops: vec![KernelOp {
816                kind: KernelOpKind::BinOpKind(BinOp::Add),
817                operands: vec![0, 0],
818                result: Some(1),
819            }],
820            child_bodies: vec![],
821            literals: vec![],
822        };
823        let desc = KernelDescriptor {
824            id: "loop_carried".into(),
825            bindings: BindingLayout { slots: vec![] },
826            dispatch: Dispatch::new(1, 1, 1),
827            body: KernelBody {
828                ops: vec![
829                    KernelOp {
830                        kind: KernelOpKind::Literal,
831                        operands: vec![0],
832                        result: Some(0),
833                    },
834                    KernelOp {
835                        kind: KernelOpKind::StructuredBlock,
836                        operands: vec![0],
837                        result: None,
838                    },
839                    KernelOp {
840                        kind: KernelOpKind::BinOpKind(BinOp::Mul),
841                        operands: vec![1, 0],
842                        result: Some(2),
843                    },
844                ],
845                child_bodies: vec![child],
846                literals: vec![LiteralValue::U32(7)],
847            },
848        };
849
850        assert_eq!(verify(&desc), Ok(()));
851    }
852
853    #[test]
854    fn run_all_output_verifies() {
855        // Full pipeline output must satisfy verify(). This is the
856        // critical regression gate  -  any rewrite that produces an
857        // invalid descriptor will fail this test.
858        let desc = empty_desc(
859            vec![
860                KernelOp {
861                    kind: KernelOpKind::Literal,
862                    operands: vec![0],
863                    result: Some(0),
864                },
865                KernelOp {
866                    kind: KernelOpKind::Literal,
867                    operands: vec![1],
868                    result: Some(1),
869                },
870                KernelOp {
871                    kind: KernelOpKind::BinOpKind(BinOp::Add),
872                    operands: vec![0, 1],
873                    result: Some(2),
874                },
875                KernelOp {
876                    kind: KernelOpKind::BinOpKind(BinOp::Mul),
877                    operands: vec![1, 0],
878                    result: Some(3),
879                },
880            ],
881            vec![LiteralValue::U32(0), LiteralValue::U32(99)],
882        );
883        let optimized = crate::rewrites::run_all(&desc);
884        assert_eq!(verify(&optimized), Ok(()));
885    }
886
887    #[test]
888    fn dispatch_zero_x_dim_detected() {
889        let desc = KernelDescriptor {
890            id: "k".into(),
891            bindings: BindingLayout { slots: vec![] },
892            dispatch: Dispatch::new(0, 1, 1),
893            body: KernelBody {
894                ops: vec![],
895                child_bodies: vec![],
896                literals: vec![],
897            },
898        };
899        let r = verify(&desc);
900        let errs = r.unwrap_err();
901        assert!(errs
902            .iter()
903            .any(|e| matches!(e.kind, VerifyErrorKind::DispatchZeroDim { axis: 0 })));
904    }
905
906    #[test]
907    fn dispatch_zero_z_dim_detected() {
908        let desc = KernelDescriptor {
909            id: "k".into(),
910            bindings: BindingLayout { slots: vec![] },
911            dispatch: Dispatch::new(64, 1, 0),
912            body: KernelBody {
913                ops: vec![],
914                child_bodies: vec![],
915                literals: vec![],
916            },
917        };
918        let r = verify(&desc);
919        let errs = r.unwrap_err();
920        assert!(errs
921            .iter()
922            .any(|e| matches!(e.kind, VerifyErrorKind::DispatchZeroDim { axis: 2 })));
923    }
924
925    #[test]
926    fn duplicate_binding_slot_detected() {
927        use crate::{BindingSlot, BindingVisibility, MemoryClass};
928        use vyre_foundation::ir::DataType;
929        let dup = BindingSlot {
930            slot: 5,
931            element_type: DataType::U32,
932            element_count: None,
933            memory_class: MemoryClass::Global,
934            visibility: BindingVisibility::ReadWrite,
935            name: "a".into(),
936        };
937        let mut second = dup.clone();
938        second.name = "b".into();
939        let desc = KernelDescriptor {
940            id: "k".into(),
941            bindings: BindingLayout {
942                slots: vec![dup, second],
943            },
944            dispatch: Dispatch::new(64, 1, 1),
945            body: KernelBody {
946                ops: vec![],
947                child_bodies: vec![],
948                literals: vec![],
949            },
950        };
951        let r = verify(&desc);
952        let errs = r.unwrap_err();
953        assert!(errs
954            .iter()
955            .any(|e| matches!(e.kind, VerifyErrorKind::DuplicateBindingSlotId { slot: 5 })));
956    }
957
958    #[test]
959    fn dispatch_normal_no_error() {
960        let desc = KernelDescriptor {
961            id: "k".into(),
962            bindings: BindingLayout { slots: vec![] },
963            dispatch: Dispatch::new(64, 1, 1),
964            body: KernelBody {
965                ops: vec![],
966                child_bodies: vec![],
967                literals: vec![],
968            },
969        };
970        assert_eq!(verify(&desc), Ok(()));
971    }
972
973    #[test]
974    fn host_binding_in_workgroup_range_is_rejected() {
975        use crate::{BindingSlot, BindingVisibility, MemoryClass};
976        use vyre_foundation::ir::DataType;
977        let bad = BindingSlot {
978            slot: crate::lower::WORKGROUP_SLOT_BASE + 7,
979            element_type: DataType::U32,
980            element_count: None,
981            memory_class: MemoryClass::Global,
982            visibility: BindingVisibility::ReadWrite,
983            name: "host_in_high_range".into(),
984        };
985        let desc = KernelDescriptor {
986            id: "k".into(),
987            bindings: BindingLayout { slots: vec![bad] },
988            dispatch: Dispatch::new(64, 1, 1),
989            body: KernelBody {
990                ops: vec![],
991                child_bodies: vec![],
992                literals: vec![],
993            },
994        };
995        let errs = verify(&desc).unwrap_err();
996        assert!(errs
997            .iter()
998            .any(|e| matches!(e.kind, VerifyErrorKind::HostBindingInWorkgroupRange { .. })));
999    }
1000
1001    #[test]
1002    fn workgroup_binding_in_host_range_is_rejected() {
1003        use crate::{BindingSlot, BindingVisibility, MemoryClass};
1004        use vyre_foundation::ir::DataType;
1005        let bad = BindingSlot {
1006            slot: 5,
1007            element_type: DataType::U32,
1008            element_count: Some(64),
1009            memory_class: MemoryClass::Shared,
1010            visibility: BindingVisibility::ReadWrite,
1011            name: "shared_in_low_range".into(),
1012        };
1013        let desc = KernelDescriptor {
1014            id: "k".into(),
1015            bindings: BindingLayout { slots: vec![bad] },
1016            dispatch: Dispatch::new(64, 1, 1),
1017            body: KernelBody {
1018                ops: vec![],
1019                child_bodies: vec![],
1020                literals: vec![],
1021            },
1022        };
1023        let errs = verify(&desc).unwrap_err();
1024        assert!(errs
1025            .iter()
1026            .any(|e| matches!(e.kind, VerifyErrorKind::WorkgroupBindingInHostRange { .. })));
1027    }
1028}