1use std::sync::Arc;
23
24use vyre_foundation::ir::model::expr::Ident;
25use vyre_foundation::ir::{BufferAccess, BufferDecl, DataType, Expr, Node, Program};
26
27pub const OP_ID: &str = "vyre-primitives::graph::toposort";
29pub const TOPOSORT_OFFSETS_BUFFER: &str = "toposort offsets";
31pub const TOPOSORT_TARGETS_BUFFER: &str = "toposort targets";
33pub const TOPOSORT_INDEGREE_SCRATCH_BUFFER: &str = "toposort indeg_scratch";
35pub const TOPOSORT_QUEUE_SCRATCH_BUFFER: &str = "toposort queue_scratch";
37pub const TOPOSORT_ORDER_OUT_BUFFER: &str = "toposort order_out";
39pub const TOPOSORT_DISPATCH_GRID: [u32; 3] = [1, 1, 1];
41
42#[derive(Debug, Clone, PartialEq, Eq)]
44#[non_exhaustive]
45pub enum ToposortError {
46 Cycle {
49 node: u32,
52 },
53 UnknownNode {
55 edge: usize,
57 node: u32,
59 },
60 IndegreeOverflow {
63 node: u32,
65 },
66 InconsistentState {
69 message: String,
71 },
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
76#[non_exhaustive]
77pub enum ToposortCsrError {
78 BadCsr {
80 message: String,
82 },
83 BadOrder {
85 message: String,
87 },
88}
89
90#[derive(Clone, Copy, Debug, Eq, PartialEq)]
95pub struct ToposortCsrLayout {
96 pub node_count: u32,
98 pub node_words: usize,
100 pub offset_words: usize,
102 pub target_words: usize,
104}
105
106#[derive(Clone, Debug, Eq, PartialEq)]
108pub struct ToposortCsrDispatchPlan {
109 pub layout: ToposortCsrLayout,
111 pub grid: [u32; 3],
113 pub offset_words: usize,
115 pub target_words: usize,
117 pub node_words: usize,
119}
120
121#[derive(Clone, Copy, Debug, Eq, PartialEq)]
128pub struct ToposortCsrStaticInputKey {
129 pub node_count: u32,
131 pub node_words: usize,
133 pub offset_words: usize,
135 pub target_words: usize,
137 pub offsets_hash: u64,
139 pub targets_hash: u64,
141}
142
143impl ToposortCsrDispatchPlan {
144 #[must_use]
146 pub fn program(&self) -> Program {
147 toposort_program(
148 self.layout.node_count,
149 TOPOSORT_OFFSETS_BUFFER,
150 TOPOSORT_TARGETS_BUFFER,
151 TOPOSORT_INDEGREE_SCRATCH_BUFFER,
152 TOPOSORT_QUEUE_SCRATCH_BUFFER,
153 TOPOSORT_ORDER_OUT_BUFFER,
154 )
155 }
156
157 pub fn static_input_key(
164 &self,
165 offsets: &[u32],
166 targets: &[u32],
167 ) -> Result<ToposortCsrStaticInputKey, ToposortCsrError> {
168 if offsets.len() != self.offset_words {
169 return Err(ToposortCsrError::BadCsr {
170 message: format!(
171 "Fix: toposort_csr static key expected {} offset words, got {}.",
172 self.offset_words,
173 offsets.len()
174 ),
175 });
176 }
177 if targets.len() != self.target_words {
178 return Err(ToposortCsrError::BadCsr {
179 message: format!(
180 "Fix: toposort_csr static key expected {} target words, got {}.",
181 self.target_words,
182 targets.len()
183 ),
184 });
185 }
186 Ok(ToposortCsrStaticInputKey {
187 node_count: self.layout.node_count,
188 node_words: self.node_words,
189 offset_words: self.offset_words,
190 target_words: self.target_words,
191 offsets_hash: toposort_csr_slice_fingerprint(offsets),
192 targets_hash: toposort_csr_slice_fingerprint(targets),
193 })
194 }
195}
196
197#[must_use]
199pub fn toposort_csr_slice_fingerprint(values: &[u32]) -> u64 {
200 super::u32_slice_fingerprint(values)
201}
202
203pub fn toposort_csr(
215 node_count: u32,
216 offsets: &[u32],
217 targets: &[u32],
218) -> Result<Vec<u32>, ToposortCsrError> {
219 let mut order = Vec::new();
220 toposort_csr_into(node_count, offsets, targets, &mut order)?;
221 Ok(order)
222}
223
224#[derive(Debug, Default, Clone)]
231pub struct ToposortCsrScratch {
232 pub indeg: Vec<u32>,
234 pub queue: Vec<u32>,
236}
237
238impl ToposortCsrScratch {
239 pub fn new() -> Self {
241 Self::default()
242 }
243}
244
245pub fn toposort_csr_into(
253 node_count: u32,
254 offsets: &[u32],
255 targets: &[u32],
256 order: &mut Vec<u32>,
257) -> Result<(), ToposortCsrError> {
258 let mut scratch = ToposortCsrScratch::default();
259 toposort_csr_into_with_scratch(node_count, offsets, targets, order, &mut scratch)
260}
261
262pub fn toposort_csr_into_with_scratch(
272 node_count: u32,
273 offsets: &[u32],
274 targets: &[u32],
275 order: &mut Vec<u32>,
276 scratch: &mut ToposortCsrScratch,
277) -> Result<(), ToposortCsrError> {
278 let layout = validate_toposort_csr_inputs(node_count, offsets, targets)?;
279 order.clear();
280 scratch.indeg.clear();
281 scratch.queue.clear();
282 if node_count == 0 {
283 return Ok(());
284 }
285
286 let node_words = layout.node_words;
287 crate::graph::scratch::reserve_graph_items_with(
288 &mut scratch.indeg,
289 node_words,
290 "toposort CSR CPU oracle",
291 "toposort_csr indegree scratch",
292 toposort_csr_allocation,
293 )?;
294 scratch.indeg.resize(node_words, 0);
295 for (idx, &target) in targets.iter().enumerate() {
296 scratch.indeg[target as usize] =
297 scratch.indeg[target as usize]
298 .checked_add(1)
299 .ok_or_else(|| ToposortCsrError::BadCsr {
300 message: format!(
301 "Fix: toposort_csr target node {target} indegree overflowed at targets[{idx}]."
302 ),
303 })?;
304 }
305
306 crate::graph::scratch::reserve_graph_items_with(
307 &mut scratch.queue,
308 node_words,
309 "toposort CSR CPU oracle",
310 "toposort_csr zero-indegree queue",
311 toposort_csr_allocation,
312 )?;
313 for node in 0..node_count {
314 if scratch.indeg[node as usize] == 0 {
315 scratch.queue.push(node);
316 }
317 }
318 crate::graph::scratch::reserve_graph_items_with(
319 order,
320 node_words,
321 "toposort CSR CPU oracle",
322 "toposort_csr output order",
323 toposort_csr_allocation,
324 )?;
325 while let Some(node) = scratch.queue.pop() {
326 order.push(node);
327 let start = offsets[node as usize] as usize;
328 let end = offsets[node as usize + 1] as usize;
329 for (edge_offset, &dependent) in targets[start..end].iter().enumerate() {
330 let slot = &mut scratch.indeg[dependent as usize];
331 *slot = slot
332 .checked_sub(1)
333 .ok_or_else(|| ToposortCsrError::BadOrder {
334 message: format!(
335 "Fix: toposort_csr indegree underflow for edge {} from {node} to {dependent}.",
336 start + edge_offset
337 ),
338 })?;
339 if *slot == 0 {
340 scratch.queue.push(dependent);
341 }
342 }
343 }
344
345 validate_toposort_csr_order_with_layout(&layout, offsets, targets, order)
346}
347
348pub fn validate_toposort_csr_inputs(
355 node_count: u32,
356 offsets: &[u32],
357 targets: &[u32],
358) -> Result<ToposortCsrLayout, ToposortCsrError> {
359 if node_count == 0 {
360 if offsets != [0] || !targets.is_empty() {
361 return Err(ToposortCsrError::BadCsr {
362 message:
363 "Fix: toposort_csr zero-node graph requires offsets == [0] and empty targets."
364 .to_string(),
365 });
366 }
367 return Ok(ToposortCsrLayout {
368 node_count,
369 node_words: 0,
370 offset_words: 1,
371 target_words: 0,
372 });
373 }
374 let expected_offsets =
375 (node_count as usize)
376 .checked_add(1)
377 .ok_or_else(|| ToposortCsrError::BadCsr {
378 message: format!(
379 "Fix: toposort_csr node_count + 1 overflows usize for node_count={node_count}."
380 ),
381 })?;
382 if offsets.len() != expected_offsets {
383 return Err(ToposortCsrError::BadCsr {
384 message: format!(
385 "Fix: toposort_csr requires offsets.len() == node_count + 1, got len={}, node_count={node_count}.",
386 offsets.len()
387 ),
388 });
389 }
390 if offsets[0] != 0 {
391 return Err(ToposortCsrError::BadCsr {
392 message: format!(
393 "Fix: toposort_csr requires offsets[0] == 0, got {}.",
394 offsets[0]
395 ),
396 });
397 }
398 for (idx, pair) in offsets.windows(2).enumerate() {
399 if pair[0] > pair[1] {
400 return Err(ToposortCsrError::BadCsr {
401 message: format!(
402 "Fix: toposort_csr offsets must be monotonic, but offsets[{idx}]={} > offsets[{}]={}.",
403 pair[0],
404 idx + 1,
405 pair[1]
406 ),
407 });
408 }
409 }
410 if offsets[node_count as usize] as usize != targets.len() {
411 return Err(ToposortCsrError::BadCsr {
412 message: format!(
413 "Fix: toposort_csr offsets[node_count] must equal targets.len(), got {} vs {}.",
414 offsets[node_count as usize],
415 targets.len()
416 ),
417 });
418 }
419 for (idx, &target) in targets.iter().enumerate() {
420 if target >= node_count {
421 return Err(ToposortCsrError::BadCsr {
422 message: format!(
423 "Fix: toposort_csr targets[{idx}]={target} is outside node_count={node_count}."
424 ),
425 });
426 }
427 }
428 Ok(ToposortCsrLayout {
429 node_count,
430 node_words: node_count as usize,
431 offset_words: expected_offsets,
432 target_words: targets.len(),
433 })
434}
435
436pub fn plan_toposort_csr_dispatch(
438 node_count: u32,
439 offsets: &[u32],
440 targets: &[u32],
441) -> Result<ToposortCsrDispatchPlan, ToposortCsrError> {
442 let layout = validate_toposort_csr_inputs(node_count, offsets, targets)?;
443 Ok(ToposortCsrDispatchPlan {
444 offset_words: layout.offset_words,
445 target_words: layout.target_words,
446 node_words: layout.node_words,
447 layout,
448 grid: TOPOSORT_DISPATCH_GRID,
449 })
450}
451
452#[cfg(test)]
453mod dispatch_plan_tests {
454 use super::*;
455
456 #[test]
457 fn dispatch_plan_owns_scratch_sizes_and_grid() {
458 let plan = plan_toposort_csr_dispatch(3, &[0, 2, 3, 3], &[1, 2, 2])
459 .expect("Fix: valid DAG CSR should plan topological-sort dispatch");
460
461 assert_eq!(plan.grid, TOPOSORT_DISPATCH_GRID);
462 assert_eq!(plan.offset_words, 4);
463 assert_eq!(plan.target_words, 3);
464 assert_eq!(plan.node_words, 3);
465 assert_eq!(plan.layout.node_count, 3);
466 }
467
468 #[test]
469 fn empty_dispatch_plan_is_non_dispatchable_but_well_shaped() {
470 let plan = plan_toposort_csr_dispatch(0, &[0], &[])
471 .expect("Fix: canonical empty CSR should plan without dispatch");
472
473 assert_eq!(plan.grid, TOPOSORT_DISPATCH_GRID);
474 assert_eq!(plan.offset_words, 1);
475 assert_eq!(plan.target_words, 0);
476 assert_eq!(plan.node_words, 0);
477 assert_eq!(plan.layout.node_count, 0);
478 }
479
480 #[test]
481 fn csr_into_emits_order_accepted_by_public_validator() {
482 let offsets = [0, 2, 3, 3];
483 let targets = [1, 2, 2];
484 let mut order = Vec::with_capacity(3);
485
486 toposort_csr_into(3, &offsets, &targets, &mut order)
487 .expect("Fix: valid DAG CSR should topologically sort.");
488
489 validate_toposort_csr_order(3, &offsets, &targets, &order)
490 .expect("Fix: toposort_csr_into output must satisfy the public order validator.");
491 assert_eq!(order.len(), 3);
492 }
493
494 #[test]
495 fn csr_order_validator_rejects_dependency_inversion() {
496 let err = validate_toposort_csr_order(3, &[0, 2, 3, 3], &[1, 2, 2], &[2, 1, 0])
497 .expect_err("Fix: dependency-inverted CSR order must be rejected.");
498
499 assert!(matches!(err, ToposortCsrError::BadOrder { .. }));
500 }
501
502 #[test]
503 fn static_input_key_tracks_content_not_only_shape() {
504 let plan = plan_toposort_csr_dispatch(4, &[0, 2, 3, 3, 3], &[1, 2, 3])
505 .expect("Fix: valid CSR should plan topological-sort dispatch");
506 let first = plan
507 .static_input_key(&[0, 2, 3, 3, 3], &[1, 2, 3])
508 .expect("Fix: static key should accept matching slices");
509 let same = plan
510 .static_input_key(&[0, 2, 3, 3, 3], &[1, 2, 3])
511 .expect("Fix: identical CSR should produce identical key");
512 let changed_targets = plan
513 .static_input_key(&[0, 2, 3, 3, 3], &[2, 3, 3])
514 .expect("Fix: same-shape CSR content change should still key");
515
516 assert_eq!(first, same);
517 assert_eq!(first.node_count, 4);
518 assert_eq!(first.node_words, 4);
519 assert_eq!(first.offset_words, 5);
520 assert_eq!(first.target_words, 3);
521 assert_ne!(first, changed_targets);
522 assert_eq!(first.offsets_hash, changed_targets.offsets_hash);
523 assert_ne!(first.targets_hash, changed_targets.targets_hash);
524 }
525
526 #[test]
527 fn static_input_key_rejects_plan_slice_drift() {
528 let plan = plan_toposort_csr_dispatch(3, &[0, 1, 2, 2], &[1, 2])
529 .expect("Fix: valid CSR should plan topological-sort dispatch");
530
531 let err = plan
532 .static_input_key(&[0, 1, 2, 2], &[1])
533 .expect_err("Fix: stale plan must not accept mismatched target slices");
534
535 assert!(matches!(err, ToposortCsrError::BadCsr { .. }));
536 }
537}
538
539pub fn validate_toposort_csr_order(
548 node_count: u32,
549 offsets: &[u32],
550 targets: &[u32],
551 order: &[u32],
552) -> Result<(), ToposortCsrError> {
553 let layout = validate_toposort_csr_inputs(node_count, offsets, targets)?;
554 validate_toposort_csr_order_with_layout(&layout, offsets, targets, order)
555}
556
557fn validate_toposort_csr_order_with_layout(
558 layout: &ToposortCsrLayout,
559 offsets: &[u32],
560 targets: &[u32],
561 order: &[u32],
562) -> Result<(), ToposortCsrError> {
563 let node_count = layout.node_count;
564 if order.len() != node_count as usize {
565 return Err(ToposortCsrError::BadOrder {
566 message: format!(
567 "Fix: toposort_csr expected {} order entries, got {}.",
568 node_count,
569 order.len()
570 ),
571 });
572 }
573 let mut pos: Vec<usize> = Vec::new();
574 crate::graph::scratch::reserve_graph_items_with(
575 &mut pos,
576 layout.node_words,
577 "toposort CSR CPU oracle",
578 "toposort_csr order positions",
579 toposort_csr_allocation,
580 )?;
581 pos.resize(layout.node_words, usize::MAX);
582 for (idx, &node) in order.iter().enumerate() {
583 if node >= node_count {
584 return Err(ToposortCsrError::BadOrder {
585 message: format!(
586 "Fix: toposort_csr order[{idx}]={node} is outside node_count={node_count}."
587 ),
588 });
589 }
590 let slot = &mut pos[node as usize];
591 if *slot != usize::MAX {
592 return Err(ToposortCsrError::BadOrder {
593 message: format!(
594 "Fix: toposort_csr order contains duplicate node {node}; graph may be cyclic or backend output is malformed."
595 ),
596 });
597 }
598 *slot = idx;
599 }
600 if let Some((missing, _)) = pos
601 .iter()
602 .enumerate()
603 .find(|(_, position)| **position == usize::MAX)
604 {
605 return Err(ToposortCsrError::BadOrder {
606 message: format!(
607 "Fix: toposort_csr order omitted node {missing}; graph may be cyclic."
608 ),
609 });
610 }
611
612 for prereq in 0..node_count {
613 let start = offsets[prereq as usize] as usize;
614 let end = offsets[prereq as usize + 1] as usize;
615 for &dependent in &targets[start..end] {
616 if pos[prereq as usize] >= pos[dependent as usize] {
617 return Err(ToposortCsrError::BadOrder {
618 message: format!(
619 "Fix: toposort_csr order violates dependency edge {prereq}->{dependent}; prerequisite position {} must be before dependent position {}.",
620 pos[prereq as usize],
621 pos[dependent as usize]
622 ),
623 });
624 }
625 }
626 }
627 Ok(())
628}
629
630pub fn toposort(node_count: u32, edges: &[(u32, u32)]) -> Result<Vec<u32>, ToposortError> {
643 const NONE: usize = usize::MAX;
644
645 validate_toposort_edge_ids(node_count, edges)?;
646
647 let n = node_count as usize;
648 let mut indeg: Vec<u32> = Vec::new();
649 crate::graph::scratch::reserve_graph_items_with(
650 &mut indeg,
651 n,
652 "toposort CPU oracle",
653 "toposort indegree scratch",
654 toposort_allocation,
655 )?;
656 indeg.resize(n, 0);
657 let mut outgoing_head: Vec<usize> = Vec::new();
658 crate::graph::scratch::reserve_graph_items_with(
659 &mut outgoing_head,
660 n,
661 "toposort CPU oracle",
662 "toposort outgoing heads",
663 toposort_allocation,
664 )?;
665 outgoing_head.resize(n, NONE);
666 let mut outgoing_to: Vec<u32> = Vec::new();
667 crate::graph::scratch::reserve_graph_items_with(
668 &mut outgoing_to,
669 edges.len(),
670 "toposort CPU oracle",
671 "toposort outgoing targets",
672 toposort_allocation,
673 )?;
674 let mut outgoing_next: Vec<usize> = Vec::new();
675 crate::graph::scratch::reserve_graph_items_with(
676 &mut outgoing_next,
677 edges.len(),
678 "toposort CPU oracle",
679 "toposort outgoing links",
680 toposort_allocation,
681 )?;
682 let mut depends_head: Vec<usize> = Vec::new();
683 crate::graph::scratch::reserve_graph_items_with(
684 &mut depends_head,
685 n,
686 "toposort CPU oracle",
687 "toposort dependency heads",
688 toposort_allocation,
689 )?;
690 depends_head.resize(n, NONE);
691 let mut depends_to: Vec<u32> = Vec::new();
692 crate::graph::scratch::reserve_graph_items_with(
693 &mut depends_to,
694 edges.len(),
695 "toposort CPU oracle",
696 "toposort dependency targets",
697 toposort_allocation,
698 )?;
699 let mut depends_next: Vec<usize> = Vec::new();
700 crate::graph::scratch::reserve_graph_items_with(
701 &mut depends_next,
702 edges.len(),
703 "toposort CPU oracle",
704 "toposort dependency links",
705 toposort_allocation,
706 )?;
707
708 for &(from, to) in edges {
709 let outgoing_idx = outgoing_to.len();
710 outgoing_to.push(from);
711 outgoing_next.push(outgoing_head[to as usize]);
712 outgoing_head[to as usize] = outgoing_idx;
713
714 let depends_idx = depends_to.len();
715 depends_to.push(to);
716 depends_next.push(depends_head[from as usize]);
717 depends_head[from as usize] = depends_idx;
718
719 indeg[from as usize] = indeg[from as usize]
720 .checked_add(1)
721 .ok_or(ToposortError::IndegreeOverflow { node: from })?;
722 }
723
724 let mut queue: Vec<u32> = Vec::new();
725 crate::graph::scratch::reserve_graph_items_with(
726 &mut queue,
727 n,
728 "toposort CPU oracle",
729 "toposort zero-indegree queue",
730 toposort_allocation,
731 )?;
732 for v in 0..node_count {
733 if indeg[v as usize] == 0 {
734 queue.push(v);
735 }
736 }
737 let mut out: Vec<u32> = Vec::new();
738 crate::graph::scratch::reserve_graph_items_with(
739 &mut out,
740 n,
741 "toposort CPU oracle",
742 "toposort output order",
743 toposort_allocation,
744 )?;
745
746 while let Some(&v) = queue.last() {
747 queue.pop();
748 out.push(v);
749 let mut edge = outgoing_head[v as usize];
750 while edge != NONE {
751 let u = outgoing_to[edge];
752 let slot = &mut indeg[u as usize];
753 *slot = slot.checked_sub(1).ok_or_else(|| {
754 ToposortError::InconsistentState {
755 message: format!(
756 "toposort indegree underflow for node {u}. Fix: rebuild dependency edges before scheduling."
757 ),
758 }
759 })?;
760 if *slot == 0 {
761 queue.push(u);
762 }
763 edge = outgoing_next[edge];
764 }
765 }
766
767 if out.len() != n {
768 let seed = indeg
775 .iter()
776 .enumerate()
777 .find(|(_, deg)| **deg > 0)
778 .map(|(i, _)| i as u32)
779 .ok_or_else(|| {
780 ToposortError::InconsistentState {
781 message: format!(
782 "toposort could not find a positive-indegree seed while output_len={} node_count={n}. Fix: rebuild dependency indegrees before scheduling.",
783 out.len()
784 ),
785 }
786 });
787 let seed = seed?;
788 let mut on_stack: Vec<bool> = Vec::new();
789 crate::graph::scratch::reserve_graph_items_with(
790 &mut on_stack,
791 n,
792 "toposort CPU oracle",
793 "toposort cycle diagnosis stack",
794 toposort_allocation,
795 )?;
796 on_stack.resize(n, false);
797 let mut cursor = seed;
798 let cycle_node = loop {
799 if on_stack[cursor as usize] {
800 break cursor;
801 }
802 on_stack[cursor as usize] = true;
803 let mut edge = depends_head[cursor as usize];
804 let mut next = None;
805 while edge != NONE {
806 let candidate = depends_to[edge];
807 if indeg[candidate as usize] > 0 {
808 next = Some(candidate);
809 break;
810 }
811 edge = depends_next[edge];
812 }
813 match next {
814 Some(n) => cursor = n,
815 None => {
816 return Err(ToposortError::InconsistentState {
817 message: format!(
818 "toposort cycle diagnosis found stuck node {cursor} without an unemitted dependency. Fix: rebuild the dependency adjacency; this state is inconsistent with Kahn's invariant."
819 ),
820 });
821 }
822 }
823 };
824 return Err(ToposortError::Cycle { node: cycle_node });
825 }
826 Ok(out)
827}
828
829fn validate_toposort_edge_ids(node_count: u32, edges: &[(u32, u32)]) -> Result<(), ToposortError> {
830 for (edge_idx, &(from, to)) in edges.iter().enumerate() {
831 if from >= node_count {
832 return Err(ToposortError::UnknownNode {
833 edge: edge_idx,
834 node: from,
835 });
836 }
837 if to >= node_count {
838 return Err(ToposortError::UnknownNode {
839 edge: edge_idx,
840 node: to,
841 });
842 }
843 }
844 Ok(())
845}
846
847fn toposort_csr_allocation(message: String) -> ToposortCsrError {
848 ToposortCsrError::BadCsr { message }
849}
850
851fn toposort_allocation(message: String) -> ToposortError {
852 ToposortError::InconsistentState { message }
853}
854
855#[must_use]
868pub fn toposort_program(
869 node_count: u32,
870 offsets_buf: &str,
871 targets_buf: &str,
872 indeg_scratch: &str,
873 queue_scratch: &str,
874 order_out: &str,
875) -> Program {
876 let lane0 = Expr::eq(Expr::InvocationId { axis: 0 }, Expr::u32(0));
877
878 let body = vec![
879 Node::loop_for(
881 "i",
882 Expr::u32(0),
883 Expr::u32(node_count),
884 vec![Node::store(indeg_scratch, Expr::var("i"), Expr::u32(0))],
885 ),
886 Node::let_bind("edge_count", Expr::load(offsets_buf, Expr::u32(node_count))),
888 Node::loop_for(
889 "e",
890 Expr::u32(0),
891 Expr::var("edge_count"),
892 vec![
893 Node::let_bind("t", Expr::load(targets_buf, Expr::var("e"))),
894 Node::let_bind("old", Expr::load(indeg_scratch, Expr::var("t"))),
895 Node::store(
896 indeg_scratch,
897 Expr::var("t"),
898 Expr::add(Expr::var("old"), Expr::u32(1)),
899 ),
900 ],
901 ),
902 Node::let_bind("write_head", Expr::u32(0)),
904 Node::loop_for(
905 "v",
906 Expr::u32(0),
907 Expr::u32(node_count),
908 vec![Node::if_then(
909 Expr::eq(Expr::load(indeg_scratch, Expr::var("v")), Expr::u32(0)),
910 vec![
911 Node::store(queue_scratch, Expr::var("write_head"), Expr::var("v")),
912 Node::assign(
913 "write_head",
914 Expr::add(Expr::var("write_head"), Expr::u32(1)),
915 ),
916 ],
917 )],
918 ),
919 Node::let_bind("read_head", Expr::u32(0)),
921 Node::let_bind("out_idx", Expr::u32(0)),
922 Node::loop_for(
923 "step",
924 Expr::u32(0),
925 Expr::u32(node_count),
926 vec![Node::if_then(
927 Expr::lt(Expr::var("read_head"), Expr::var("write_head")),
928 vec![
929 Node::let_bind("v", Expr::load(queue_scratch, Expr::var("read_head"))),
930 Node::assign("read_head", Expr::add(Expr::var("read_head"), Expr::u32(1))),
931 Node::store(order_out, Expr::var("out_idx"), Expr::var("v")),
932 Node::assign("out_idx", Expr::add(Expr::var("out_idx"), Expr::u32(1))),
933 Node::let_bind("edge_start", Expr::load(offsets_buf, Expr::var("v"))),
934 Node::let_bind(
935 "edge_end",
936 Expr::load(offsets_buf, Expr::add(Expr::var("v"), Expr::u32(1))),
937 ),
938 Node::loop_for(
939 "e",
940 Expr::var("edge_start"),
941 Expr::var("edge_end"),
942 vec![
943 Node::let_bind("u", Expr::load(targets_buf, Expr::var("e"))),
944 Node::let_bind(
945 "new_deg",
946 Expr::sub(Expr::load(indeg_scratch, Expr::var("u")), Expr::u32(1)),
947 ),
948 Node::store(indeg_scratch, Expr::var("u"), Expr::var("new_deg")),
949 Node::if_then(
950 Expr::eq(Expr::var("new_deg"), Expr::u32(0)),
951 vec![
952 Node::store(
953 queue_scratch,
954 Expr::var("write_head"),
955 Expr::var("u"),
956 ),
957 Node::assign(
958 "write_head",
959 Expr::add(Expr::var("write_head"), Expr::u32(1)),
960 ),
961 ],
962 ),
963 ],
964 ),
965 ],
966 )],
967 ),
968 ];
969
970 Program::wrapped(
971 vec![
972 BufferDecl::storage(offsets_buf, 0, BufferAccess::ReadOnly, DataType::U32)
973 .with_count(node_count.saturating_add(1)),
974 BufferDecl::storage(targets_buf, 1, BufferAccess::ReadOnly, DataType::U32),
975 BufferDecl::storage(indeg_scratch, 2, BufferAccess::ReadWrite, DataType::U32)
976 .with_count(node_count.max(1)),
977 BufferDecl::storage(queue_scratch, 3, BufferAccess::ReadWrite, DataType::U32)
978 .with_count(node_count.max(1)),
979 BufferDecl::storage(order_out, 4, BufferAccess::ReadWrite, DataType::U32)
980 .with_count(node_count.max(1)),
981 ],
982 [1, 1, 1],
983 vec![Node::Region {
984 generator: Ident::from(OP_ID),
985 source_region: None,
986 body: Arc::new(vec![Node::if_then(lane0, body)]),
987 }],
988 )
989}
990
991#[cfg(test)]
992mod tests {
993 use super::*;
994
995 #[test]
996 fn empty_graph_sorts_to_empty() {
997 assert_eq!(toposort(0, &[]), Ok(Vec::new()));
998 }
999
1000 #[test]
1001 fn no_edges_returns_all_nodes() {
1002 let got = toposort(3, &[])
1003 .expect("Fix: no-cycle case; restore this invariant before continuing.");
1004 assert_eq!(got.len(), 3);
1005 let mut sorted = got.clone();
1006 sorted.sort_unstable();
1007 assert_eq!(sorted, vec![0, 1, 2]);
1008 }
1009
1010 #[test]
1011 fn linear_chain_respects_order() {
1012 let got = toposort(3, &[(0, 1), (1, 2)])
1014 .expect("Fix: linear chain is acyclic; restore this invariant before continuing.");
1015 let pos = |v: u32| got.iter().position(|&x| x == v).unwrap();
1016 assert!(pos(2) < pos(1));
1017 assert!(pos(1) < pos(0));
1018 }
1019
1020 #[test]
1021 fn cycle_is_rejected() {
1022 let err = toposort(2, &[(0, 1), (1, 0)]).expect_err("2-cycle must be detected");
1023 assert!(matches!(err, ToposortError::Cycle { .. }));
1024 }
1025
1026 #[test]
1027 fn cycle_diagnostic_names_node_on_cycle_not_downstream() {
1028 let err = toposort(4, &[(0, 1), (1, 2), (2, 3), (3, 1)])
1034 .expect_err("3-cycle with downstream consumer must be detected");
1035 match err {
1036 ToposortError::Cycle { node } => {
1037 assert!(
1038 matches!(node, 1..=3),
1039 "cycle node {node} must be on the cycle {{1,2,3}}, not the downstream node 0"
1040 );
1041 }
1042 other => panic!("expected Cycle error, got {other:?}"),
1043 }
1044 }
1045
1046 #[test]
1047 fn unknown_node_surfaces_edge_index() {
1048 let err = toposort(2, &[(0, 5)]).expect_err("node 5 is out of range");
1049 match err {
1050 ToposortError::UnknownNode { edge, node } => {
1051 assert_eq!(edge, 0);
1052 assert_eq!(node, 5);
1053 }
1054 _ => panic!("expected UnknownNode"),
1055 }
1056 }
1057
1058 #[test]
1059 fn diamond_graph_sorts() {
1060 let got = toposort(4, &[(0, 1), (0, 2), (1, 3), (2, 3)])
1062 .expect("Fix: diamond is acyclic; restore this invariant before continuing.");
1063 let pos = |v: u32| got.iter().position(|&x| x == v).unwrap();
1064 assert!(pos(3) < pos(1));
1065 assert!(pos(3) < pos(2));
1066 assert!(pos(1) < pos(0));
1067 assert!(pos(2) < pos(0));
1068 }
1069
1070 #[test]
1071 fn emitted_program_has_expected_buffers_and_workgroup_size() {
1072 let p = toposort_program(4, "offsets", "targets", "indeg", "queue", "order");
1073 assert_eq!(p.workgroup_size, [1, 1, 1]);
1074 let names: Vec<&str> = p.buffers.iter().map(|b| b.name()).collect();
1075 assert_eq!(names, vec!["offsets", "targets", "indeg", "queue", "order"]);
1076 assert_eq!(p.buffers[0].count(), 5); assert_eq!(p.buffers[2].count(), 4); assert_eq!(p.buffers[3].count(), 4); assert_eq!(p.buffers[4].count(), 4); }
1081
1082 #[test]
1083 fn csr_reference_sorts_prerequisites_before_dependents() {
1084 let order = toposort_csr(3, &[0, 2, 3, 3], &[1, 2, 2]).unwrap();
1085 let pos = |v: u32| order.iter().position(|&x| x == v).unwrap();
1086 assert!(pos(0) < pos(1));
1087 assert!(pos(0) < pos(2));
1088 assert!(pos(1) < pos(2));
1089 }
1090
1091 #[test]
1092 fn csr_reference_reuses_output_storage() {
1093 let mut order = Vec::with_capacity(8);
1094 toposort_csr_into(3, &[0, 1, 2, 2], &[1, 2], &mut order).unwrap();
1095 let capacity = order.capacity();
1096 assert_eq!(order.len(), 3);
1097
1098 toposort_csr_into(2, &[0, 1, 1], &[1], &mut order).unwrap();
1099 assert_eq!(order.capacity(), capacity);
1100 assert_eq!(order.len(), 2);
1101 }
1102
1103 #[test]
1104 fn csr_reference_with_scratch_reuses_storage_and_clears_stale_state() {
1105 let mut order = Vec::with_capacity(8);
1106 order.extend_from_slice(&[99, 98, 97]);
1107 let mut queue = Vec::with_capacity(8);
1108 queue.extend_from_slice(&[6, 5, 4]);
1109 let mut scratch = ToposortCsrScratch {
1110 indeg: vec![7; 8],
1111 queue,
1112 };
1113 let order_capacity = order.capacity();
1114 let indeg_capacity = scratch.indeg.capacity();
1115 let queue_capacity = scratch.queue.capacity();
1116
1117 toposort_csr_into_with_scratch(4, &[0, 2, 3, 3, 3], &[1, 2, 3], &mut order, &mut scratch)
1118 .expect("Fix: valid DAG must sort while reusing caller-owned scratch.");
1119
1120 validate_toposort_csr_order(4, &[0, 2, 3, 3, 3], &[1, 2, 3], &order)
1121 .expect("Fix: scratch-backed topological order must satisfy the CSR contract.");
1122 assert_eq!(order.capacity(), order_capacity);
1123 assert_eq!(scratch.indeg.capacity(), indeg_capacity);
1124 assert_eq!(scratch.queue.capacity(), queue_capacity);
1125 assert_eq!(
1126 scratch.indeg,
1127 vec![0, 0, 0, 0],
1128 "Fix: scratch-backed traversal must not retain stale indegree counts."
1129 );
1130 assert!(
1131 scratch.queue.is_empty(),
1132 "Fix: scratch-backed traversal must consume stale and live queue entries."
1133 );
1134
1135 toposort_csr_into_with_scratch(2, &[0, 1, 1], &[1], &mut order, &mut scratch)
1136 .expect("Fix: second smaller DAG must reuse the same workspace.");
1137 validate_toposort_csr_order(2, &[0, 1, 1], &[1], &order)
1138 .expect("Fix: reused workspace must not leak prior graph state.");
1139 assert_eq!(order.capacity(), order_capacity);
1140 assert_eq!(scratch.indeg.capacity(), indeg_capacity);
1141 assert_eq!(scratch.queue.capacity(), queue_capacity);
1142 assert_eq!(scratch.indeg, vec![0, 0]);
1143 assert!(scratch.queue.is_empty());
1144 }
1145
1146 #[test]
1147 fn csr_reference_with_scratch_validates_before_mutating_reused_storage() {
1148 let mut order = vec![9, 8, 7];
1149 let mut scratch = ToposortCsrScratch {
1150 indeg: vec![1, 2],
1151 queue: vec![3],
1152 };
1153 let err = toposort_csr_into_with_scratch(2, &[0, 2, 1], &[1], &mut order, &mut scratch)
1154 .expect_err("Fix: malformed CSR offsets must be rejected.");
1155
1156 assert!(matches!(err, ToposortCsrError::BadCsr { .. }));
1157 assert_eq!(
1158 order,
1159 vec![9, 8, 7],
1160 "Fix: validation failures must not clobber reusable output storage."
1161 );
1162 assert_eq!(
1163 scratch.indeg,
1164 vec![1, 2],
1165 "Fix: validation failures must not clear reusable indegree scratch."
1166 );
1167 assert_eq!(
1168 scratch.queue,
1169 vec![3],
1170 "Fix: validation failures must not clear reusable queue scratch."
1171 );
1172 }
1173
1174 #[test]
1175 fn generated_csr_reference_with_scratch_matches_allocating_reference() {
1176 let mut order = Vec::new();
1177 let mut scratch = ToposortCsrScratch::new();
1178
1179 for case in 0..2048usize {
1180 let n = case % 17;
1181 let mut offsets = Vec::with_capacity(n + 1);
1182 let mut targets = Vec::new();
1183 offsets.push(0);
1184 for src in 0..n {
1185 for dst in src + 1..n {
1186 let mixed = case
1187 .wrapping_mul(31)
1188 .wrapping_add(src.wrapping_mul(17))
1189 .wrapping_add(dst.wrapping_mul(13));
1190 if mixed % 5 == 0 || (case % 11 == 0 && dst == src + 1) {
1191 targets.push(dst as u32);
1192 }
1193 }
1194 offsets.push(targets.len() as u32);
1195 }
1196
1197 let expected = toposort_csr(n as u32, &offsets, &targets)
1198 .expect("Fix: generated lower-triangular CSR graph must be a valid DAG.");
1199 toposort_csr_into_with_scratch(n as u32, &offsets, &targets, &mut order, &mut scratch)
1200 .expect("Fix: scratch-backed oracle must accept every generated valid DAG.");
1201 assert_eq!(
1202 order, expected,
1203 "Fix: scratch-backed oracle diverged from allocating oracle at generated case {case}."
1204 );
1205 }
1206 }
1207
1208 #[test]
1209 fn csr_validation_rejects_bad_shape() {
1210 let err = validate_toposort_csr_inputs(2, &[0, 2, 1], &[1]).unwrap_err();
1211 assert!(matches!(err, ToposortCsrError::BadCsr { .. }));
1212 }
1213
1214 #[test]
1215 fn csr_validation_returns_dispatch_layout() {
1216 assert_eq!(
1217 validate_toposort_csr_inputs(3, &[0, 2, 3, 3], &[1, 2, 2]).unwrap(),
1218 ToposortCsrLayout {
1219 node_count: 3,
1220 node_words: 3,
1221 offset_words: 4,
1222 target_words: 3,
1223 }
1224 );
1225 assert_eq!(
1226 validate_toposort_csr_inputs(0, &[0], &[]).unwrap(),
1227 ToposortCsrLayout {
1228 node_count: 0,
1229 node_words: 0,
1230 offset_words: 1,
1231 target_words: 0,
1232 }
1233 );
1234 }
1235
1236 #[test]
1237 fn csr_order_validation_rejects_duplicate_backend_output() {
1238 let err = validate_toposort_csr_order(3, &[0, 1, 2, 2], &[1, 2], &[0, 1, 1]).unwrap_err();
1239 assert!(matches!(err, ToposortCsrError::BadOrder { .. }));
1240 }
1241
1242 #[test]
1243 fn csr_order_validation_rejects_dependency_inversion() {
1244 let err = validate_toposort_csr_order(2, &[0, 1, 1], &[1], &[1, 0]).unwrap_err();
1245 assert!(matches!(err, ToposortCsrError::BadOrder { .. }));
1246 }
1247
1248 #[test]
1253 fn single_node_no_edges() {
1254 assert_eq!(toposort(1, &[]), Ok(vec![0]));
1255 }
1256
1257 #[test]
1258 fn self_loops_only_rejected() {
1259 let err = toposort(3, &[(0, 0), (1, 1), (2, 2)]).expect_err("self-loops are cycles");
1261 assert!(matches!(err, ToposortError::Cycle { .. }));
1262 }
1263
1264 #[test]
1265 fn disconnected_components_sorts_all() {
1266 let got = toposort(4, &[(0, 1), (2, 3)]).unwrap();
1268 assert_eq!(got.len(), 4);
1269 let pos = |v: u32| got.iter().position(|&x| x == v).unwrap();
1270 assert!(pos(1) < pos(0), "1 must come before 0");
1271 assert!(pos(3) < pos(2), "3 must come before 2");
1272 }
1273
1274 #[test]
1275 fn max_node_count_min_edges() {
1276 let got = toposort(1000, &[(0, 1)]).unwrap();
1278 assert_eq!(got.len(), 1000);
1279 let pos = |v: u32| got.iter().position(|&x| x == v).unwrap();
1280 assert!(pos(1) < pos(0), "1 must come before 0");
1281 }
1282
1283 #[test]
1284 fn cycle_on_large_graph_diagnostic_is_on_cycle() {
1285 let mut edges: Vec<(u32, u32)> = (0..99).map(|i| (i, i + 1)).collect();
1287 edges.push((99, 50));
1288 let err = toposort(100, &edges).expect_err("cycle must be detected");
1289 match err {
1290 ToposortError::Cycle { node } => {
1291 assert!(
1292 (50..=99).contains(&node),
1293 "cycle node {node} must be on the back-edge cycle"
1294 );
1295 }
1296 other => panic!("expected Cycle, got {other:?}"),
1297 }
1298 }
1299}