1use crate::crate_graph::CrateGraph;
17use crate::crate_schema::{crate_edge_col, crate_node_col};
18use crate::schema::{edge_col, node_col};
19use arrow::array::{Array, BooleanArray, RecordBatch, StringArray};
20use std::collections::{HashMap, HashSet, VecDeque};
21
22pub fn sort_crates(graph: &CrateGraph) -> Result<Vec<String>, String> {
30 crate::crate_graph::topo_sort_crates(graph)
31}
32
33pub fn sort_crates_parallel(graph: &CrateGraph) -> Result<Vec<Vec<String>>, String> {
39 let id_col = graph
40 .crate_nodes
41 .column(crate_node_col::ID)
42 .as_any()
43 .downcast_ref::<StringArray>()
44 .ok_or("CrateNode id column is not StringArray")?;
45
46 let wm_col = graph
47 .crate_nodes
48 .column(crate_node_col::WORKSPACE_MEMBER)
49 .as_any()
50 .downcast_ref::<BooleanArray>()
51 .ok_or("CrateNode workspace_member column is not BooleanArray")?;
52
53 let workspace_members: HashSet<String> = (0..id_col.len())
54 .filter(|&i| wm_col.value(i))
55 .map(|i| id_col.value(i).to_string())
56 .collect();
57
58 build_parallel_layers(&workspace_members, |layers| {
59 wire_crate_edges(graph, &workspace_members, layers)
60 })
61}
62
63pub fn sort_functions_in_crate(
74 crate_name: &str,
75 nodes: &RecordBatch,
76 edges: &RecordBatch,
77) -> Result<Vec<String>, String> {
78 let (node_ids, adjacency, in_degree) = build_function_graph(crate_name, nodes, edges)?;
79 kahn_sort(node_ids, adjacency, in_degree)
80}
81
82pub fn sort_functions_parallel(
88 crate_name: &str,
89 nodes: &RecordBatch,
90 edges: &RecordBatch,
91) -> Result<Vec<Vec<String>>, String> {
92 let (node_ids, adjacency, in_degree) = build_function_graph(crate_name, nodes, edges)?;
93 kahn_layers(node_ids, adjacency, in_degree)
94}
95
96#[derive(Debug)]
100pub struct ParallelismStats {
101 pub layer_count: usize,
103 pub max_layer_width: usize,
105 pub critical_path: Vec<String>,
107}
108
109pub fn crate_parallelism_stats(graph: &CrateGraph) -> Result<ParallelismStats, String> {
111 let layers = sort_crates_parallel(graph)?;
112 let max_layer_width = layers.iter().map(|l| l.len()).max().unwrap_or(0);
113 let layer_count = layers.len();
114
115 let id_col = graph
118 .crate_nodes
119 .column(crate_node_col::ID)
120 .as_any()
121 .downcast_ref::<StringArray>()
122 .ok_or("CrateNode id column is not StringArray")?;
123
124 let wm_col = graph
125 .crate_nodes
126 .column(crate_node_col::WORKSPACE_MEMBER)
127 .as_any()
128 .downcast_ref::<BooleanArray>()
129 .ok_or("CrateNode workspace_member column is not BooleanArray")?;
130
131 let workspace_members: HashSet<String> = (0..id_col.len())
132 .filter(|&i| wm_col.value(i))
133 .map(|i| id_col.value(i).to_string())
134 .collect();
135
136 let mut fwd: HashMap<String, Vec<String>> = HashMap::new();
138 for name in &workspace_members {
139 fwd.entry(name.clone()).or_default();
140 }
141 wire_crate_edges(graph, &workspace_members, &mut fwd);
142
143 let flat = sort_crates(graph)?;
145 let mut dist: HashMap<String, usize> = HashMap::new();
146 let mut prev: HashMap<String, String> = HashMap::new();
147 for name in &flat {
148 dist.insert(name.clone(), 0);
149 }
150 for name in &flat {
151 let d = dist[name];
152 for nbr in fwd.get(name).into_iter().flatten() {
153 if workspace_members.contains(nbr) {
154 let entry = dist.entry(nbr.clone()).or_insert(0);
155 if d + 1 > *entry {
156 *entry = d + 1;
157 prev.insert(nbr.clone(), name.clone());
158 }
159 }
160 }
161 }
162
163 let sink = dist
165 .iter()
166 .max_by_key(|(_, v)| *v)
167 .map(|(k, _)| k.clone())
168 .unwrap_or_default();
169
170 let mut path = Vec::new();
171 let mut cur = sink.clone();
172 loop {
173 path.push(cur.clone());
174 match prev.get(&cur) {
175 Some(p) => cur = p.clone(),
176 None => break,
177 }
178 }
179 path.reverse();
180
181 Ok(ParallelismStats {
182 layer_count,
183 max_layer_width,
184 critical_path: path,
185 })
186}
187
188fn wire_crate_edges(
192 graph: &CrateGraph,
193 workspace_members: &HashSet<String>,
194 adjacency: &mut HashMap<String, Vec<String>>,
195) {
196 let source_col = graph
197 .crate_edges
198 .column(crate_edge_col::SOURCE)
199 .as_any()
200 .downcast_ref::<StringArray>();
201 let target_col = graph
202 .crate_edges
203 .column(crate_edge_col::TARGET)
204 .as_any()
205 .downcast_ref::<StringArray>();
206 let sk_col = graph
207 .crate_edges
208 .column(crate_edge_col::SOURCE_KIND)
209 .as_any()
210 .downcast_ref::<StringArray>();
211 let dev_col = graph
212 .crate_edges
213 .column(crate_edge_col::DEV_DEP)
214 .as_any()
215 .downcast_ref::<BooleanArray>();
216
217 let (Some(src), Some(tgt), Some(sk), Some(dev)) = (source_col, target_col, sk_col, dev_col)
218 else {
219 return;
220 };
221
222 for i in 0..src.len() {
223 let kind = sk.value(i);
224 if kind != "path" && kind != "workspace" {
225 continue;
226 }
227 if dev.value(i) {
228 continue;
229 }
230 let s = src.value(i).to_string();
231 let t = tgt.value(i).to_string();
232 if workspace_members.contains(&s) && workspace_members.contains(&t) {
233 adjacency.entry(t).or_default().push(s);
237 }
238 }
239}
240
241fn kahn_sort(
243 nodes: HashSet<String>,
244 adjacency: HashMap<String, Vec<String>>,
245 in_degree: HashMap<String, usize>,
246) -> Result<Vec<String>, String> {
247 let layers = kahn_layers(nodes, adjacency, in_degree)?;
248 Ok(layers.into_iter().flatten().collect())
249}
250
251fn kahn_layers(
253 nodes: HashSet<String>,
254 adjacency: HashMap<String, Vec<String>>,
255 in_degree: HashMap<String, usize>,
256) -> Result<Vec<Vec<String>>, String> {
257 let mut in_deg = in_degree;
258 let mut adj = adjacency;
259
260 let mut queue: VecDeque<String> = nodes
261 .iter()
262 .filter(|n| in_deg.get(*n).copied().unwrap_or(0) == 0)
263 .cloned()
264 .collect();
265 let mut queue_vec: Vec<String> = queue.drain(..).collect();
267 queue_vec.sort();
268 let mut queue: VecDeque<String> = queue_vec.into();
269
270 let mut layers: Vec<Vec<String>> = Vec::new();
271 let mut visited = 0usize;
272
273 while !queue.is_empty() {
274 let mut current_layer: Vec<String> = queue.drain(..).collect();
275 current_layer.sort();
276 visited += current_layer.len();
277
278 let mut next_layer_candidates: Vec<String> = Vec::new();
279 for node in ¤t_layer {
280 for nbr in adj.remove(node).unwrap_or_default() {
281 let deg = in_deg.entry(nbr.clone()).or_insert(0);
282 if *deg > 0 {
283 *deg -= 1;
284 }
285 if *deg == 0 {
286 next_layer_candidates.push(nbr);
287 }
288 }
289 }
290 next_layer_candidates.sort();
291 next_layer_candidates.dedup();
293 layers.push(current_layer);
294 queue.extend(next_layer_candidates);
295 }
296
297 if visited < nodes.len() {
298 return Err(format!(
299 "cyclic dependency detected: {} nodes in cycle(s) — {} sorted of {}",
300 nodes.len() - visited,
301 visited,
302 nodes.len()
303 ));
304 }
305
306 Ok(layers)
307}
308
309type FunctionGraph = (
311 HashSet<String>,
312 HashMap<String, Vec<String>>,
313 HashMap<String, usize>,
314);
315
316fn build_function_graph(
320 crate_name: &str,
321 nodes: &RecordBatch,
322 edges: &RecordBatch,
323) -> Result<FunctionGraph, String> {
324 let id_col = nodes
325 .column(node_col::ID)
326 .as_any()
327 .downcast_ref::<StringArray>()
328 .ok_or("CodeNode id column is not StringArray")?;
329
330 let file_col = nodes
331 .column(node_col::FILE_PATH)
332 .as_any()
333 .downcast_ref::<StringArray>()
334 .ok_or("CodeNode file_path column is not StringArray")?;
335
336 let crate_prefix = format!("crates/{}/", crate_name);
338
339 let mut node_ids: HashSet<String> = HashSet::new();
340 let mut id_set: HashSet<String> = HashSet::new();
341 for i in 0..id_col.len() {
342 let fp = file_col.value(i);
343 if fp.starts_with(&crate_prefix) {
344 let id = id_col.value(i).to_string();
345 node_ids.insert(id.clone());
346 id_set.insert(id);
347 }
348 }
349
350 let src_col = edges
352 .column(edge_col::SOURCE_ID)
353 .as_any()
354 .downcast_ref::<StringArray>()
355 .ok_or("CodeEdge source_id column is not StringArray")?;
356 let tgt_col = edges
357 .column(edge_col::TARGET_ID)
358 .as_any()
359 .downcast_ref::<StringArray>()
360 .ok_or("CodeEdge target_id column is not StringArray")?;
361 let pred_array = arrow::compute::cast(
364 edges.column(edge_col::PREDICATE),
365 &arrow::datatypes::DataType::Utf8,
366 )
367 .map_err(|e| format!("Failed to cast predicate column to Utf8: {e}"))?;
368 let pred_col = pred_array
369 .as_any()
370 .downcast_ref::<StringArray>()
371 .ok_or("CodeEdge predicate column is not StringArray after cast")?;
372
373 let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
374 let mut in_degree: HashMap<String, usize> = HashMap::new();
375 for id in &node_ids {
376 adjacency.entry(id.clone()).or_default();
377 in_degree.entry(id.clone()).or_insert(0);
378 }
379
380 for i in 0..src_col.len() {
381 let pred = pred_col.value(i);
382 if pred != "calls" && pred != "uses" {
384 continue;
385 }
386 let src = src_col.value(i).to_string();
387 let tgt = tgt_col.value(i).to_string();
388 if id_set.contains(&src) && id_set.contains(&tgt) && src != tgt {
389 adjacency.entry(tgt).or_default().push(src.clone());
393 *in_degree.entry(src).or_insert(0) += 1;
394 }
395 }
396
397 Ok((node_ids, adjacency, in_degree))
398}
399
400fn build_parallel_layers(
403 workspace_members: &HashSet<String>,
404 wire: impl Fn(&mut HashMap<String, Vec<String>>),
405) -> Result<Vec<Vec<String>>, String> {
406 let mut adjacency: HashMap<String, Vec<String>> = HashMap::new();
407 let mut in_degree: HashMap<String, usize> = HashMap::new();
408
409 for name in workspace_members {
410 adjacency.entry(name.clone()).or_default();
411 in_degree.entry(name.clone()).or_insert(0);
412 }
413
414 wire(&mut adjacency);
415
416 let mut in_deg: HashMap<String, usize> = HashMap::new();
418 for name in workspace_members {
419 in_deg.entry(name.clone()).or_insert(0);
420 }
421 for targets in adjacency.values() {
422 for t in targets {
423 if workspace_members.contains(t) {
424 *in_deg.entry(t.clone()).or_insert(0) += 1;
425 }
426 }
427 }
428
429 kahn_layers(workspace_members.clone(), adjacency, in_deg)
430}
431
432#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::crate_graph::build_crate_graph;
438 use std::path::PathBuf;
439
440 fn workspace_root() -> PathBuf {
441 PathBuf::from(env!("CARGO_MANIFEST_DIR"))
442 .parent()
443 .unwrap()
444 .parent()
445 .unwrap()
446 .to_path_buf()
447 }
448
449 #[test]
450 fn test_sort_crates_wraps_topo_sort() {
451 let graph = build_crate_graph(&workspace_root()).expect("build_crate_graph should succeed");
452 let order = sort_crates(&graph).expect("no cycles expected");
453 assert!(!order.is_empty(), "should return at least one crate");
454 let core_pos = order.iter().position(|n| n == "nusy-arrow-core");
456 let being_pos = order.iter().position(|n| n == "nusy-being");
457 if let (Some(c), Some(b)) = (core_pos, being_pos) {
458 assert!(c < b, "nusy-arrow-core must come before nusy-being");
459 }
460 }
461
462 #[test]
463 fn test_sort_crates_parallel_layers_are_independent() {
464 let graph = build_crate_graph(&workspace_root()).expect("build_crate_graph should succeed");
465 let layers = sort_crates_parallel(&graph).expect("no cycles expected");
466
467 assert!(!layers.is_empty(), "should have at least one layer");
468
469 let total: usize = layers.iter().map(|l| l.len()).sum();
471 assert!(total > 0, "at least some workspace crates returned");
472 let flat: HashSet<String> = layers.iter().flatten().cloned().collect();
473 assert_eq!(
474 flat.len(),
475 total,
476 "no duplicates across layers: {} unique, {} total",
477 flat.len(),
478 total
479 );
480
481 let layer_index: HashMap<String, usize> = layers
484 .iter()
485 .enumerate()
486 .flat_map(|(i, layer)| layer.iter().map(move |name| (name.clone(), i)))
487 .collect();
488
489 use crate::crate_schema::crate_edge_col;
490 use arrow::array::StringArray;
491 let src_col = graph
492 .crate_edges
493 .column(crate_edge_col::SOURCE)
494 .as_any()
495 .downcast_ref::<StringArray>()
496 .expect("source column is StringArray");
497 let tgt_col = graph
498 .crate_edges
499 .column(crate_edge_col::TARGET)
500 .as_any()
501 .downcast_ref::<StringArray>()
502 .expect("target column is StringArray");
503 let dev_col = graph
504 .crate_edges
505 .column(crate_edge_col::DEV_DEP)
506 .as_any()
507 .downcast_ref::<BooleanArray>()
508 .expect("dev_dep column is BooleanArray");
509
510 for i in 0..src_col.len() {
513 if dev_col.value(i) {
514 continue; }
516 let dependent = src_col.value(i); let dep = tgt_col.value(i); let (Some(&dep_layer), Some(&dependent_layer)) =
520 (layer_index.get(dep), layer_index.get(dependent))
521 else {
522 continue; };
524 assert!(
525 dep_layer < dependent_layer,
526 "layer independence violated: '{dependent}' depends on '{dep}' but '{dep}' is in layer {dep_layer} and '{dependent}' is in layer {dependent_layer}; dependency must be in earlier layer"
527 );
528 }
529 }
530
531 #[test]
532 fn test_sort_crates_parallel_nusy_arrow_core_in_first_layers() {
533 let graph = build_crate_graph(&workspace_root()).expect("build_crate_graph should succeed");
534 let layers = sort_crates_parallel(&graph).expect("no cycles");
535
536 let core_layer = layers
538 .iter()
539 .position(|l| l.contains(&"nusy-arrow-core".to_string()));
540 assert!(
541 core_layer.is_some(),
542 "nusy-arrow-core must appear in parallel layers"
543 );
544 assert!(
545 core_layer.unwrap() <= 1,
546 "nusy-arrow-core is a root crate, should be in layer 0 or 1 (got {})",
547 core_layer.unwrap()
548 );
549 }
550
551 #[test]
552 fn test_sort_crates_parallel_count_matches_flat_sort() {
553 let graph = build_crate_graph(&workspace_root()).expect("build_crate_graph should succeed");
554 let flat = sort_crates(&graph).expect("no cycles");
555 let layers = sort_crates_parallel(&graph).expect("no cycles");
556 let parallel_total: usize = layers.iter().map(|l| l.len()).sum();
557 assert_eq!(
558 flat.len(),
559 parallel_total,
560 "flat sort and parallel sort must cover the same crates"
561 );
562 }
563
564 #[test]
565 fn test_detects_cyclic_dependency() {
566 use crate::crate_schema::{crate_edge_schema, crate_node_schema};
567 use arrow::array::{BooleanArray, RecordBatch, StringArray};
568 use std::sync::Arc;
569
570 let nodes = RecordBatch::try_new(
572 crate_node_schema(),
573 vec![
574 Arc::new(StringArray::from(vec!["crate-a", "crate-b"])),
575 Arc::new(StringArray::from(vec!["0.1.0", "0.1.0"])),
576 Arc::new(BooleanArray::from(vec![true, true])),
577 Arc::new(StringArray::from(vec![None::<&str>, None])),
578 Arc::new(StringArray::from(vec!["2021", "2021"])),
579 ],
580 )
581 .expect("build test nodes");
582
583 let edges = RecordBatch::try_new(
584 crate_edge_schema(),
585 vec![
586 Arc::new(StringArray::from(vec!["crate-a", "crate-b"])),
587 Arc::new(StringArray::from(vec!["crate-b", "crate-a"])),
588 Arc::new(StringArray::from(vec!["*", "*"])),
589 Arc::new(BooleanArray::from(vec![false, false])),
590 Arc::new(BooleanArray::from(vec![false, false])),
591 Arc::new(BooleanArray::from(vec![false, false])),
592 Arc::new(StringArray::from(vec!["path", "path"])),
593 ],
594 )
595 .expect("build test edges");
596
597 let graph = CrateGraph {
598 crate_nodes: nodes,
599 crate_edges: edges,
600 };
601
602 let result = sort_crates(&graph);
603 assert!(result.is_err(), "cycle should be detected");
604 let err = result.unwrap_err();
605 assert!(
606 err.contains("cycle") || err.contains("cyclic"),
607 "error message should mention cycle: {err}"
608 );
609 }
610
611 #[test]
612 fn test_parallelism_stats() {
613 let graph = build_crate_graph(&workspace_root()).expect("build_crate_graph should succeed");
614 let stats = crate_parallelism_stats(&graph).expect("stats should succeed");
615
616 assert!(
618 stats.layer_count >= 3,
619 "expected >= 3 layers, got {}",
620 stats.layer_count
621 );
622 assert!(
623 stats.max_layer_width >= 2,
624 "expected max_layer_width >= 2, got {}",
625 stats.max_layer_width
626 );
627 assert!(
628 !stats.critical_path.is_empty(),
629 "critical path should not be empty"
630 );
631 assert!(
632 stats.critical_path.len() >= 2,
633 "critical path should span at least 2 crates"
634 );
635 }
636
637 #[test]
640 fn test_sort_functions_callee_before_caller() {
641 use crate::schema::{CodeEdge, CodeEdgePredicate, CodeNode, CodeNodeKind};
642 use crate::schema::{build_code_edges_batch, build_code_nodes_batch};
643
644 let nodes = build_code_nodes_batch(&[
646 CodeNode {
647 id: "fn_a".to_string(),
648 kind: CodeNodeKind::Function,
649 name: "a".to_string(),
650 file_path: Some("crates/tc/src/lib.rs".to_string()),
651 ..Default::default()
652 },
653 CodeNode {
654 id: "fn_b".to_string(),
655 kind: CodeNodeKind::Function,
656 name: "b".to_string(),
657 file_path: Some("crates/tc/src/lib.rs".to_string()),
658 ..Default::default()
659 },
660 ])
661 .expect("build nodes");
662
663 let edges = build_code_edges_batch(&[CodeEdge {
665 source_id: "fn_a".to_string(),
666 target_id: "fn_b".to_string(),
667 predicate: CodeEdgePredicate::Calls,
668 weight: None,
669 commit_id: None,
670 }])
671 .expect("build edges");
672
673 let order = sort_functions_in_crate("tc", &nodes, &edges).expect("no cycle");
674
675 assert!(
676 order.contains(&"fn_a".to_string()),
677 "fn_a must be in sort output"
678 );
679 assert!(
680 order.contains(&"fn_b".to_string()),
681 "fn_b must be in sort output"
682 );
683
684 let pos_a = order.iter().position(|id| id == "fn_a").unwrap();
685 let pos_b = order.iter().position(|id| id == "fn_b").unwrap();
686 assert!(
687 pos_b < pos_a,
688 "callee fn_b (pos {pos_b}) must come before caller fn_a (pos {pos_a})"
689 );
690 }
691
692 #[test]
693 fn test_sort_functions_parallel() {
694 use crate::schema::build_code_nodes_batch;
695 use crate::schema::{CodeNode, CodeNodeKind, code_edges_schema};
696 use arrow::array::RecordBatch;
697 use std::sync::Arc;
698
699 let nodes = build_code_nodes_batch(&[
702 CodeNode {
703 id: "fn_x".to_string(),
704 kind: CodeNodeKind::Function,
705 name: "x".to_string(),
706 file_path: Some("crates/ind/src/lib.rs".to_string()),
707 ..Default::default()
708 },
709 CodeNode {
710 id: "fn_y".to_string(),
711 kind: CodeNodeKind::Function,
712 name: "y".to_string(),
713 file_path: Some("crates/ind/src/lib.rs".to_string()),
714 ..Default::default()
715 },
716 ])
717 .expect("build nodes");
718
719 let edges = RecordBatch::new_empty(Arc::new(code_edges_schema()));
721
722 let layers = sort_functions_parallel("ind", &nodes, &edges).expect("no cycle");
723
724 let all_ids: Vec<String> = layers.iter().flatten().cloned().collect();
725 assert!(
726 all_ids.contains(&"fn_x".to_string()),
727 "fn_x must appear in parallel output"
728 );
729 assert!(
730 all_ids.contains(&"fn_y".to_string()),
731 "fn_y must appear in parallel output"
732 );
733 assert_eq!(
735 layers.len(),
736 1,
737 "two independent functions should be in one layer"
738 );
739 assert_eq!(layers[0].len(), 2, "both functions in layer 0");
740 }
741
742 #[test]
743 fn test_sort_functions_parallel_callee_in_earlier_layer() {
744 use crate::schema::{CodeEdge, CodeEdgePredicate, CodeNode, CodeNodeKind};
745 use crate::schema::{build_code_edges_batch, build_code_nodes_batch};
746
747 let nodes = build_code_nodes_batch(&[
750 CodeNode {
751 id: "fn_root".to_string(),
752 kind: CodeNodeKind::Function,
753 name: "root".to_string(),
754 file_path: Some("crates/tc2/src/lib.rs".to_string()),
755 ..Default::default()
756 },
757 CodeNode {
758 id: "fn_helper".to_string(),
759 kind: CodeNodeKind::Function,
760 name: "helper".to_string(),
761 file_path: Some("crates/tc2/src/lib.rs".to_string()),
762 ..Default::default()
763 },
764 CodeNode {
765 id: "fn_leaf".to_string(),
766 kind: CodeNodeKind::Function,
767 name: "leaf".to_string(),
768 file_path: Some("crates/tc2/src/lib.rs".to_string()),
769 ..Default::default()
770 },
771 ])
772 .expect("build nodes");
773
774 let edges = build_code_edges_batch(&[
775 CodeEdge {
776 source_id: "fn_root".to_string(),
777 target_id: "fn_helper".to_string(),
778 predicate: CodeEdgePredicate::Calls,
779 weight: None,
780 commit_id: None,
781 },
782 CodeEdge {
783 source_id: "fn_helper".to_string(),
784 target_id: "fn_leaf".to_string(),
785 predicate: CodeEdgePredicate::Calls,
786 weight: None,
787 commit_id: None,
788 },
789 ])
790 .expect("build edges");
791
792 let layers = sort_functions_parallel("tc2", &nodes, &edges).expect("no cycle");
793
794 let layer_index: HashMap<String, usize> = layers
795 .iter()
796 .enumerate()
797 .flat_map(|(i, layer)| layer.iter().map(move |id| (id.clone(), i)))
798 .collect();
799
800 let root_layer = *layer_index.get("fn_root").expect("fn_root in layers");
801 let helper_layer = *layer_index.get("fn_helper").expect("fn_helper in layers");
802 let leaf_layer = *layer_index.get("fn_leaf").expect("fn_leaf in layers");
803
804 assert!(
805 leaf_layer < helper_layer,
806 "leaf (layer {leaf_layer}) must be in earlier layer than helper (layer {helper_layer})"
807 );
808 assert!(
809 helper_layer < root_layer,
810 "helper (layer {helper_layer}) must be in earlier layer than root (layer {root_layer})"
811 );
812 }
813}