@@ -7,7 +7,7 @@ use bevy_utils::default;
77#[ cfg( feature = "trace" ) ]
88use bevy_utils:: tracing:: info_span;
99use bevy_utils:: {
10- petgraph:: prelude:: * ,
10+ petgraph:: { algo :: TarjanScc , prelude:: * } ,
1111 thiserror:: Error ,
1212 tracing:: { error, warn} ,
1313 HashMap , HashSet ,
@@ -942,7 +942,7 @@ impl ScheduleGraph {
942942
943943 // check hierarchy for cycles
944944 self . hierarchy . topsort = self
945- . topsort_graph ( & self . hierarchy . graph )
945+ . topsort_graph ( & self . hierarchy . graph , ReportCycles :: Hierarchy )
946946 . map_err ( |_| ScheduleBuildError :: HierarchyCycle ) ?;
947947
948948 let hier_results = check_graph ( & self . hierarchy . graph , & self . hierarchy . topsort ) ;
@@ -960,7 +960,7 @@ impl ScheduleGraph {
960960
961961 // check dependencies for cycles
962962 self . dependency . topsort = self
963- . topsort_graph ( & self . dependency . graph )
963+ . topsort_graph ( & self . dependency . graph , ReportCycles :: Dependency )
964964 . map_err ( |_| ScheduleBuildError :: DependencyCycle ) ?;
965965
966966 // check for systems or system sets depending on sets they belong to
@@ -1083,7 +1083,7 @@ impl ScheduleGraph {
10831083
10841084 // topsort
10851085 self . dependency_flattened . topsort = self
1086- . topsort_graph ( & dependency_flattened)
1086+ . topsort_graph ( & dependency_flattened, ReportCycles :: Dependency )
10871087 . map_err ( |_| ScheduleBuildError :: DependencyCycle ) ?;
10881088 self . dependency_flattened . graph = dependency_flattened;
10891089
@@ -1318,6 +1318,12 @@ impl ScheduleGraph {
13181318 }
13191319}
13201320
1321+ /// Used to select the appropriate reporting function.
1322+ enum ReportCycles {
1323+ Hierarchy ,
1324+ Dependency ,
1325+ }
1326+
13211327// methods for reporting errors
13221328impl ScheduleGraph {
13231329 fn get_node_name ( & self , id : & NodeId ) -> String {
@@ -1345,7 +1351,7 @@ impl ScheduleGraph {
13451351 name
13461352 }
13471353
1348- fn get_node_kind ( id : & NodeId ) -> & ' static str {
1354+ fn get_node_kind ( & self , id : & NodeId ) -> & ' static str {
13491355 match id {
13501356 NodeId :: System ( _) => "system" ,
13511357 NodeId :: Set ( _) => "system set" ,
@@ -1366,7 +1372,7 @@ impl ScheduleGraph {
13661372 writeln ! (
13671373 message,
13681374 " -- {:?} '{:?}' cannot be child of set '{:?}', longer path exists" ,
1369- Self :: get_node_kind( child) ,
1375+ self . get_node_kind( child) ,
13701376 self . get_node_name( child) ,
13711377 self . get_node_name( parent) ,
13721378 )
@@ -1376,48 +1382,95 @@ impl ScheduleGraph {
13761382 error ! ( "{}" , message) ;
13771383 }
13781384
1379- /// Get topology sorted [`NodeId`], also ensures the graph contains no cycle
1380- /// returns Err(()) if there are cycles
1381- fn topsort_graph ( & self , graph : & DiGraphMap < NodeId , ( ) > ) -> Result < Vec < NodeId > , ( ) > {
1382- // tarjan_scc's run order is reverse topological order
1383- let mut rev_top_sorted_nodes = Vec :: < NodeId > :: with_capacity ( graph. node_count ( ) ) ;
1384- let mut tarjan_scc = bevy_utils:: petgraph:: algo:: TarjanScc :: new ( ) ;
1385- let mut sccs_with_cycle = Vec :: < Vec < NodeId > > :: new ( ) ;
1385+ /// Tries to topologically sort `graph`.
1386+ ///
1387+ /// If the graph is acyclic, returns [`Ok`] with the list of [`NodeId`] in a valid
1388+ /// topological order. If the graph contains cycles, returns [`Err`] with the list of
1389+ /// strongly-connected components that contain cycles (also in a valid topological order).
1390+ ///
1391+ /// # Errors
1392+ ///
1393+ /// If the graph contain cycles, then an error is returned.
1394+ fn topsort_graph (
1395+ & self ,
1396+ graph : & DiGraphMap < NodeId , ( ) > ,
1397+ report : ReportCycles ,
1398+ ) -> Result < Vec < NodeId > , Vec < Vec < NodeId > > > {
1399+ // Tarjan's SCC algorithm returns elements in *reverse* topological order.
1400+ let mut tarjan_scc = TarjanScc :: new ( ) ;
1401+ let mut top_sorted_nodes = Vec :: with_capacity ( graph. node_count ( ) ) ;
1402+ let mut sccs_with_cycles = Vec :: new ( ) ;
13861403
13871404 tarjan_scc. run ( graph, |scc| {
1388- // by scc's definition, each scc is the cluster of nodes that they can reach each other
1389- // so scc with size larger than 1, means there is/are cycle(s).
1405+ // A strongly-connected component is a group of nodes who can all reach each other
1406+ // through one or more paths. If an SCC contains more than one node, there must be
1407+ // at least one cycle within them.
13901408 if scc. len ( ) > 1 {
1391- sccs_with_cycle . push ( scc. to_vec ( ) ) ;
1409+ sccs_with_cycles . push ( scc. to_vec ( ) ) ;
13921410 }
1393- rev_top_sorted_nodes . extend_from_slice ( scc) ;
1411+ top_sorted_nodes . extend_from_slice ( scc) ;
13941412 } ) ;
13951413
1396- if sccs_with_cycle. is_empty ( ) {
1397- // reverse the reverted to get topological order
1398- let mut top_sorted_nodes = rev_top_sorted_nodes;
1414+ if sccs_with_cycles. is_empty ( ) {
1415+ // reverse to get topological order
13991416 top_sorted_nodes. reverse ( ) ;
14001417 Ok ( top_sorted_nodes)
14011418 } else {
1402- self . report_cycles ( & sccs_with_cycle) ;
1403- Err ( ( ) )
1419+ let mut cycles = Vec :: new ( ) ;
1420+ for scc in & sccs_with_cycles {
1421+ cycles. append ( & mut simple_cycles_in_component ( graph, scc) ) ;
1422+ }
1423+
1424+ match report {
1425+ ReportCycles :: Hierarchy => self . report_hierarchy_cycles ( & cycles) ,
1426+ ReportCycles :: Dependency => self . report_dependency_cycles ( & cycles) ,
1427+ }
1428+
1429+ Err ( sccs_with_cycles)
14041430 }
14051431 }
14061432
1407- /// Print detailed cycle messages
1408- fn report_cycles ( & self , sccs_with_cycles : & [ Vec < NodeId > ] ) {
1409- let mut message = format ! (
1410- "schedule contains at least {} cycle(s)" ,
1411- sccs_with_cycles. len( )
1412- ) ;
1433+ /// Logs details of cycles in the hierarchy graph.
1434+ fn report_hierarchy_cycles ( & self , cycles : & [ Vec < NodeId > ] ) {
1435+ let mut message = format ! ( "schedule has {} in_set cycle(s):\n " , cycles. len( ) ) ;
1436+ for ( i, cycle) in cycles. iter ( ) . enumerate ( ) {
1437+ let mut names = cycle. iter ( ) . map ( |id| self . get_node_name ( id) ) ;
1438+ let first_name = names. next ( ) . unwrap ( ) ;
1439+ writeln ! (
1440+ message,
1441+ "cycle {}: set '{first_name}' contains itself" ,
1442+ i + 1 ,
1443+ )
1444+ . unwrap ( ) ;
1445+ writeln ! ( message, "set '{first_name}'" ) . unwrap ( ) ;
1446+ for name in names. chain ( std:: iter:: once ( first_name) ) {
1447+ writeln ! ( message, " ... which contains set '{name}'" ) . unwrap ( ) ;
1448+ }
1449+ writeln ! ( message) . unwrap ( ) ;
1450+ }
1451+
1452+ error ! ( "{}" , message) ;
1453+ }
14131454
1414- writeln ! ( message, " -- cycle(s) found within:" ) . unwrap ( ) ;
1415- for ( i, scc) in sccs_with_cycles. iter ( ) . enumerate ( ) {
1416- let names = scc
1455+ /// Logs details of cycles in the dependency graph.
1456+ fn report_dependency_cycles ( & self , cycles : & [ Vec < NodeId > ] ) {
1457+ let mut message = format ! ( "schedule has {} before/after cycle(s):\n " , cycles. len( ) ) ;
1458+ for ( i, cycle) in cycles. iter ( ) . enumerate ( ) {
1459+ let mut names = cycle
14171460 . iter ( )
1418- . map ( |id| self . get_node_name ( id) )
1419- . collect :: < Vec < _ > > ( ) ;
1420- writeln ! ( message, " ---- {i}: {names:?}" ) . unwrap ( ) ;
1461+ . map ( |id| ( self . get_node_kind ( id) , self . get_node_name ( id) ) ) ;
1462+ let ( first_kind, first_name) = names. next ( ) . unwrap ( ) ;
1463+ writeln ! (
1464+ message,
1465+ "cycle {}: {first_kind} '{first_name}' must run before itself" ,
1466+ i + 1 ,
1467+ )
1468+ . unwrap ( ) ;
1469+ writeln ! ( message, "{first_kind} '{first_name}'" ) . unwrap ( ) ;
1470+ for ( kind, name) in names. chain ( std:: iter:: once ( ( first_kind, first_name) ) ) {
1471+ writeln ! ( message, " ... which must run before {kind} '{name}'" ) . unwrap ( ) ;
1472+ }
1473+ writeln ! ( message) . unwrap ( ) ;
14211474 }
14221475
14231476 error ! ( "{}" , message) ;
0 commit comments