use crate::abi::types::{TypeDef, TypeKind};
use std::collections::{BTreeMap, BTreeSet, VecDeque};
#[cfg(feature = "layout_graph_trace")]
fn trace_log(msg: impl AsRef<str>) {
eprintln!("[layout_graph] {}", msg.as_ref());
}
#[cfg(not(feature = "layout_graph_trace"))]
fn trace_log(_msg: impl AsRef<str>) {}
#[derive(Debug)]
pub struct LayoutGraph {
nodes: BTreeMap<String, LayoutGraphNode>,
}
#[derive(Debug, Clone)]
pub struct LayoutGraphNode {
pub id: usize,
pub name: String,
pub deps: BTreeSet<String>,
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum LayoutGraphError {
#[error("circular dependency detected: {0:?}")]
CircularDependency(Vec<String>),
}
impl LayoutGraph {
pub fn build(typedefs: &[TypeDef]) -> Self {
let mut nodes = BTreeMap::new();
for (idx, typedef) in typedefs.iter().enumerate() {
let mut deps = BTreeSet::new();
collect_dependencies(&typedef.kind, &mut deps);
deps.remove(&typedef.name); nodes.insert(
typedef.name.clone(),
LayoutGraphNode {
id: idx,
name: typedef.name.clone(),
deps,
},
);
}
Self { nodes }
}
pub fn topo_order(&self) -> Result<Vec<String>, LayoutGraphError> {
let mut in_degree: BTreeMap<String, usize> = BTreeMap::new();
let mut adjacency: BTreeMap<String, Vec<String>> = BTreeMap::new();
for (name, node) in &self.nodes {
in_degree.entry(name.clone()).or_insert(0);
for dep in &node.deps {
adjacency.entry(dep.clone()).or_default().push(name.clone());
*in_degree.entry(name.clone()).or_insert(0) += 1;
}
}
let mut queue: VecDeque<String> = in_degree
.iter()
.filter_map(|(name, degree)| {
if *degree == 0 {
Some(name.clone())
} else {
None
}
})
.collect();
let mut order = Vec::with_capacity(self.nodes.len());
while let Some(name) = queue.pop_front() {
trace_log(format!("processing node {name}"));
order.push(name.clone());
if let Some(children) = adjacency.get(&name) {
for child in children {
if let Some(degree) = in_degree.get_mut(child) {
*degree = degree.saturating_sub(1);
if *degree == 0 {
trace_log(format!("enqueue {child}"));
queue.push_back(child.clone());
}
}
}
}
}
if order.len() == self.nodes.len() {
Ok(order)
} else {
let cycle: Vec<String> = in_degree
.into_iter()
.filter_map(|(name, degree)| if degree > 0 { Some(name) } else { None })
.collect();
Err(LayoutGraphError::CircularDependency(cycle))
}
}
pub fn nodes(&self) -> impl Iterator<Item = &LayoutGraphNode> {
self.nodes.values()
}
}
fn collect_dependencies(kind: &TypeKind, deps: &mut BTreeSet<String>) {
match kind {
TypeKind::Primitive(_) => {}
TypeKind::TypeRef(type_ref) => {
deps.insert(type_ref.name.clone());
}
TypeKind::Struct(struct_type) => {
for field in &struct_type.fields {
collect_dependencies(&field.field_type, deps);
}
}
TypeKind::Union(union_type) => {
for variant in &union_type.variants {
collect_dependencies(&variant.variant_type, deps);
}
}
TypeKind::Enum(enum_type) => {
for variant in &enum_type.variants {
collect_dependencies(&variant.variant_type, deps);
}
}
TypeKind::Array(array_type) => {
collect_dependencies(&array_type.element_type, deps);
}
TypeKind::SizeDiscriminatedUnion(sdu_type) => {
for variant in &sdu_type.variants {
collect_dependencies(&variant.variant_type, deps);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::abi::expr::{ExprKind, FieldRefExpr, LiteralExpr};
use crate::abi::types::{
ArrayType, EnumType, PrimitiveType, StructField, StructType, TypeRefType, UnionType,
};
use crate::abi::types::{EnumVariant, IntegralType};
#[test]
fn layout_graph_topological_order() {
let typedefs = vec![
TypeDef {
name: "B".to_string(),
format: None,
kind: TypeKind::Primitive(PrimitiveType::Integral(IntegralType::U32)),
},
TypeDef {
name: "A".to_string(),
format: None,
kind: TypeKind::Struct(StructType {
container_attributes: Default::default(),
fields: vec![StructField {
name: "field_b".to_string(),
format: None,
field_type: TypeKind::TypeRef(TypeRefType {
name: "B".to_string(),
package: None,
comment: None,
}),
}],
}),
},
];
let graph = LayoutGraph::build(&typedefs);
let order = graph.topo_order().unwrap();
assert_eq!(order, vec!["B".to_string(), "A".to_string()]);
}
#[test]
fn layout_graph_detects_cycle() {
let typedefs = vec![
TypeDef {
name: "X".to_string(),
format: None,
kind: TypeKind::Struct(StructType {
container_attributes: Default::default(),
fields: vec![StructField {
name: "y".to_string(),
format: None,
field_type: TypeKind::TypeRef(TypeRefType {
name: "Y".to_string(),
package: None,
comment: None,
}),
}],
}),
},
TypeDef {
name: "Y".to_string(),
format: None,
kind: TypeKind::Struct(StructType {
container_attributes: Default::default(),
fields: vec![StructField {
name: "x".to_string(),
format: None,
field_type: TypeKind::TypeRef(TypeRefType {
name: "X".to_string(),
package: None,
comment: None,
}),
}],
}),
},
];
let graph = LayoutGraph::build(&typedefs);
let err = graph.topo_order().unwrap_err();
assert!(matches!(err, LayoutGraphError::CircularDependency(_)));
}
#[test]
fn collects_nested_dependencies() {
let typedefs = vec![
TypeDef {
name: "Leaf".to_string(),
format: None,
kind: TypeKind::Primitive(PrimitiveType::Integral(IntegralType::U8)),
},
TypeDef {
name: "Node".to_string(),
format: None,
kind: TypeKind::Enum(EnumType {
container_attributes: Default::default(),
tag_ref: ExprKind::FieldRef(crate::abi::expr::FieldRefExpr {
path: vec!["tag".to_string()],
}),
variants: vec![crate::abi::types::EnumVariant {
name: "leaf_variant".to_string(),
tag_value: 0,
variant_type: TypeKind::Array(ArrayType {
container_attributes: Default::default(),
size: ExprKind::Literal(crate::abi::expr::LiteralExpr::U8(1)),
element_type: Box::new(TypeKind::TypeRef(TypeRefType {
name: "Leaf".to_string(),
package: None,
comment: None,
})),
jagged: false,
}),
}],
}),
},
];
let graph = LayoutGraph::build(&typedefs);
let order = graph.topo_order().unwrap();
assert_eq!(order, vec!["Leaf".to_string(), "Node".to_string()]);
}
#[test]
fn deterministic_order_with_multiple_roots() {
let typedefs = vec![
TypeDef {
name: "C".to_string(),
format: None,
kind: TypeKind::Primitive(PrimitiveType::Integral(IntegralType::U8)),
},
TypeDef {
name: "A".to_string(),
format: None,
kind: TypeKind::Primitive(PrimitiveType::Integral(IntegralType::U8)),
},
TypeDef {
name: "B".to_string(),
format: None,
kind: TypeKind::Primitive(PrimitiveType::Integral(IntegralType::U8)),
},
];
let graph = LayoutGraph::build(&typedefs);
let order = graph.topo_order().unwrap();
assert_eq!(
order,
vec!["A".to_string(), "B".to_string(), "C".to_string()]
);
}
#[test]
fn allows_recursive_reference_via_nested_struct() {
let typedefs = vec![TypeDef {
name: "Node".to_string(),
format: None,
kind: TypeKind::Struct(StructType {
container_attributes: Default::default(),
fields: vec![
StructField {
name: "value".to_string(),
format: None,
field_type: TypeKind::Primitive(PrimitiveType::Integral(IntegralType::U8)),
},
StructField {
name: "child".to_string(),
format: None,
field_type: TypeKind::Struct(StructType {
container_attributes: Default::default(),
fields: vec![StructField {
name: "parent_link".to_string(),
format: None,
field_type: TypeKind::TypeRef(TypeRefType {
name: "Node".to_string(),
package: None,
comment: None,
}),
}],
}),
},
],
}),
}];
let graph = LayoutGraph::build(&typedefs);
let order = graph.topo_order().unwrap();
assert_eq!(order, vec!["Node".to_string()]);
}
#[test]
fn detects_illegal_forward_reference_cycle() {
let typedefs = vec![
TypeDef {
name: "Parent".to_string(),
format: None,
kind: TypeKind::Struct(StructType {
container_attributes: Default::default(),
fields: vec![
StructField {
name: "header".to_string(),
format: None,
field_type: TypeKind::Primitive(PrimitiveType::Integral(
IntegralType::U8,
)),
},
StructField {
name: "child".to_string(),
format: None,
field_type: TypeKind::TypeRef(TypeRefType {
name: "Child".to_string(),
package: None,
comment: None,
}),
},
],
}),
},
TypeDef {
name: "Child".to_string(),
format: None,
kind: TypeKind::Struct(StructType {
container_attributes: Default::default(),
fields: vec![
StructField {
name: "tag".to_string(),
format: None,
field_type: TypeKind::Primitive(PrimitiveType::Integral(
IntegralType::U8,
)),
},
StructField {
name: "payload".to_string(),
format: None,
field_type: TypeKind::TypeRef(TypeRefType {
name: "Parent".to_string(),
package: None,
comment: None,
}),
},
],
}),
},
];
let graph = LayoutGraph::build(&typedefs);
let err = graph.topo_order().unwrap_err();
assert!(matches!(err, LayoutGraphError::CircularDependency(cycle) if cycle.len() == 2));
}
}