From 2c8c2b3a3224372af10322961393d5f6a6ebe5d8 Mon Sep 17 00:00:00 2001 From: Hironobu Sano Date: Tue, 18 Aug 2026 14:11:36 +0900 Subject: [PATCH 1/2] fix(mir): preserve shared canonical graph traversal --- crates/align_mir/src/canonical_graph.rs | 53 +++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/crates/align_mir/src/canonical_graph.rs b/crates/align_mir/src/canonical_graph.rs index ff941182..d03824f7 100644 --- a/crates/align_mir/src/canonical_graph.rs +++ b/crates/align_mir/src/canonical_graph.rs @@ -515,10 +515,15 @@ impl<'a> GraphValidator<'a> { reverse.entry(edge.to).or_default().push(edge.from); } + // Mark a node when it is entered, not when its siblings are scheduled. Marking every + // child while pushing a whole sibling list can make a shared DAG look cyclic in the + // second pass: a later parent sees its child as already finished even though its finish + // order was never recorded. The entry-time mark also leaves active back-edges for the + // SCC pass, so genuine self and mutual cycles remain rejected. let mut seen = HashSet::new(); let mut finish = Vec::with_capacity(self.order.len()); for &start in &self.order { - if !seen.insert(start) { + if seen.contains(&start) { continue; } let mut work = vec![(start, false)]; @@ -527,12 +532,13 @@ impl<'a> GraphValidator<'a> { finish.push(node); continue; } + if !seen.insert(node) { + continue; + } work.push((node, true)); if let Some(children) = forward.get(&node) { for &child in children.iter().rev() { - if seen.insert(child) { - work.push((child, false)); - } + work.push((child, false)); } } } @@ -3216,6 +3222,45 @@ mod tests { ); } + #[test] + fn canonical_graph_allows_shared_inline_dag() { + let mut program = baseline_program(); + let field = |name: &str, ty: Ty| align_sema::hir::FieldDef { + name: name.to_string(), + ty, + }; + let child = align_sema::hir::StructDef { + name: "Child".to_string(), + source_name: "Child".to_string(), + fields: vec![field("value", Ty::Str)], + align: None, + c_repr: false, + }; + let parent = align_sema::hir::StructDef { + name: "Parent".to_string(), + source_name: "Parent".to_string(), + fields: vec![field("child", Ty::Option(Scalar::Struct(1)))], + align: None, + c_repr: false, + }; + let root = align_sema::hir::StructDef { + name: "Root".to_string(), + source_name: "Root".to_string(), + fields: vec![ + field("direct", Ty::Option(Scalar::Struct(1))), + field("nested", Ty::Option(Scalar::Struct(2))), + ], + align: None, + c_repr: false, + }; + program.structs = vec![root, child, parent]; + + assert_eq!( + validate(Ty::Struct(0), &program).unwrap(), + [Node::Struct(0), Node::Struct(1), Node::Struct(2)] + ); + } + #[test] fn canonical_graph_validation_error_precedence() { let base = baseline_program(); From 33e6c252c3a88027a3e3c52029c874f088b669b0 Mon Sep 17 00:00:00 2001 From: Hironobu Sano Date: Tue, 18 Aug 2026 14:20:41 +0900 Subject: [PATCH 2/2] fix(mir): exercise shared inline DAG regression --- crates/align_mir/src/canonical_graph.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/align_mir/src/canonical_graph.rs b/crates/align_mir/src/canonical_graph.rs index d03824f7..e55ba42f 100644 --- a/crates/align_mir/src/canonical_graph.rs +++ b/crates/align_mir/src/canonical_graph.rs @@ -3247,8 +3247,8 @@ mod tests { name: "Root".to_string(), source_name: "Root".to_string(), fields: vec![ - field("direct", Ty::Option(Scalar::Struct(1))), field("nested", Ty::Option(Scalar::Struct(2))), + field("direct", Ty::Option(Scalar::Struct(1))), ], align: None, c_repr: false, @@ -3256,8 +3256,8 @@ mod tests { program.structs = vec![root, child, parent]; assert_eq!( - validate(Ty::Struct(0), &program).unwrap(), - [Node::Struct(0), Node::Struct(1), Node::Struct(2)] + validate(Ty::Struct(0), &program), + Ok(vec![Node::Struct(0), Node::Struct(2), Node::Struct(1)]) ); }