Skip to content

Commit 49ab774

Browse files
committed
perf(analyzer): analyze instantiated modules once
Why: the comb-loop pass visited every source template before recursively rebuilding concrete children for parent summaries. On the aggregate analyze benchmark, that repeated graph construction dominated the new post-pass and triggered CodSpeed's 5.87% regression. Start from source roots, use each concrete child graph for both its diagnostics and parent summary, and retain fallback analysis for uninstantiated modules and every distinct generic specialization.
1 parent 7497471 commit 49ab774

2 files changed

Lines changed: 119 additions & 9 deletions

File tree

crates/analyzer/src/comb_loop_detect.rs

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@
4545
//! spans and regular repeated copies have symbolic nodes, so graph size is
4646
//! normally a function of accesses and boundaries rather than declared bit
4747
//! width or unpacked-array length.
48-
//! 3. Child modules are analyzed before parents. A module summary contains only
49-
//! input-to-output feedthrough proven in the child graph. Instance actuals
50-
//! project the child's regions back into the parent, preserving bit
51-
//! positions, aggregate layout, and Cartesian repetition axes when that
48+
//! 3. Traversal starts at source modules which are not instantiated by another
49+
//! source template. Concrete child modules are analyzed before their parent;
50+
//! the same child graph emits definition-local diagnostics and produces its
51+
//! input-to-output summary, so the standalone template is not rebuilt.
52+
//! Uninstantiated modules are roots and remain independently checked.
53+
//! Instance actuals project child regions back into the parent, preserving
54+
//! bit positions, aggregate layout, and Cartesian repetition axes when that
5255
//! mapping is representable.
5356
//! 4. Iterative SCC discovery finds cyclic components without using the process
5457
//! stack. For each SCC, diagnostics choose a stable source-backed edge and
@@ -456,6 +459,7 @@ enum ComponentSummaryKey {
456459
struct CombSummaryCache {
457460
modules: HashMap<ComponentSummaryKey, ModuleCombSummary>,
458461
procedures: crate::comb_memory_ssa::ProcedureSummaryCache,
462+
analyzed_templates: HashSet<StrId>,
459463
}
460464

461465
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
@@ -527,12 +531,21 @@ fn analyze(ir: &Ir, collect_coverage: bool) -> (CombAnalysisResult, Vec<Analyzer
527531
let order = module_analysis_order(ir);
528532
for &idx in &order {
529533
if let Component::Module(module) = &ir.components[idx] {
534+
// A concrete instance analysis already checked this source
535+
// template and produced the summary its parent consumes. Generic
536+
// specializations are still all visited by
537+
// `ensure_instance_summaries`; this skips only the redundant
538+
// standalone template pass.
539+
if summaries.analyzed_templates.contains(&module.name) {
540+
continue;
541+
}
530542
// Unevaluable generic params do not have a concrete module shape.
531543
if module.suppress_unassigned {
532544
incomplete.push(IncompleteCombAnalysis {
533545
module: module.name.to_string(),
534546
reasons: [IncompleteReason::UnevaluatedGeneric].into(),
535547
});
548+
summaries.analyzed_templates.insert(module.name);
536549
continue;
537550
}
538551
ensure_instance_summaries(
@@ -552,6 +565,7 @@ fn analyze(ir: &Ir, collect_coverage: bool) -> (CombAnalysisResult, Vec<Analyzer
552565
);
553566
extend_unique_errors(&mut coverage, module_coverage);
554567
check_graph(module, &graph, &mut loops);
568+
summaries.analyzed_templates.insert(module.name);
555569
if !reasons.is_empty() {
556570
incomplete.push(IncompleteCombAnalysis {
557571
module: module.name.to_string(),
@@ -648,6 +662,7 @@ fn ensure_instance_summaries(
648662
);
649663
extend_unique_errors(coverage, child_coverage);
650664
check_graph(child, &graph, loops);
665+
summaries.analyzed_templates.insert(child.name);
651666
let summary = compute_module_summary(child, &graph, &bit_part);
652667
summaries.modules.insert(key.clone(), summary);
653668
if !reasons.is_empty() {
@@ -695,7 +710,24 @@ fn module_analysis_order(ir: &Ir) -> Vec<usize> {
695710
.iter()
696711
.map(|component| matches!(component, Component::Module(_)))
697712
.collect::<Vec<_>>();
698-
order_from_dependencies(&is_module, &deps, &rev_deps)
713+
let order = order_from_dependencies(&is_module, &deps, &rev_deps);
714+
prioritize_module_roots(order, &rev_deps)
715+
}
716+
717+
fn prioritize_module_roots(
718+
order: Vec<usize>,
719+
reverse_dependencies: &[HashSet<usize>],
720+
) -> Vec<usize> {
721+
// Start from modules which are not instantiated by another source
722+
// template. Their recursive summary walk checks concrete children before
723+
// those children's redundant standalone entries are encountered. Keep
724+
// the previous deterministic order within both groups; a name-cycle has
725+
// no root and remains in the fallback group.
726+
let (mut roots, remaining): (Vec<_>, Vec<_>) = order
727+
.into_iter()
728+
.partition(|index| reverse_dependencies[*index].is_empty());
729+
roots.extend(remaining);
730+
roots
699731
}
700732

701733
fn order_from_dependencies(
@@ -3501,6 +3533,12 @@ mod memory_ssa_tests {
35013533

35023534
let order = order_from_dependencies(&is_module, &deps, &rev_deps);
35033535
assert_eq!(order, vec![1, 3, 0, 2]);
3536+
3537+
// Why this case exists: concrete instance summaries are discovered
3538+
// only while visiting a parent. Roots must therefore run before the
3539+
// child-first fallback order, while a source-name cycle remains in a
3540+
// deterministic fallback position instead of disappearing.
3541+
assert_eq!(prioritize_module_roots(order, &rev_deps), vec![3, 2, 1, 0]);
35043542
}
35053543

35063544
#[test]

crates/analyzer/src/tests.rs

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10992,6 +10992,77 @@ fn comb_loop_distinguishes_generic_module_specializations() {
1099210992
);
1099310993
}
1099410994

10995+
#[test]
10996+
fn comb_loop_analyzes_each_instantiated_generic_specialization() {
10997+
// Why this case exists: seeing one concrete child may suppress the
10998+
// redundant source-template pass, but it must not suppress a different
10999+
// concrete specialization. The disabled instance is intentionally first;
11000+
// the enabled instance still closes a real parent feedback path.
11001+
assert_comb_loop(
11002+
"each concrete generic-module specialization receives its own summary",
11003+
r#"
11004+
module Child #(
11005+
param ENABLE: u32 = 0,
11006+
)(
11007+
i: input logic,
11008+
o: output logic,
11009+
) {
11010+
if ENABLE :g_enabled {
11011+
assign o = i;
11012+
} else {
11013+
assign o = 0;
11014+
}
11015+
}
11016+
module Top (
11017+
o: output logic<2>,
11018+
) {
11019+
var feedback: logic<2>;
11020+
var passed : logic<2>;
11021+
inst disabled: Child #(
11022+
ENABLE: 0,
11023+
)(
11024+
i: feedback[0],
11025+
o: passed[0],
11026+
);
11027+
inst enabled: Child #(
11028+
ENABLE: 1,
11029+
)(
11030+
i: feedback[1],
11031+
o: passed[1],
11032+
);
11033+
assign feedback = passed;
11034+
assign o = feedback;
11035+
}
11036+
"#,
11037+
true,
11038+
);
11039+
}
11040+
11041+
#[test]
11042+
fn comb_loop_analyzes_uninstantiated_modules() {
11043+
// Why this case exists: roots are analyzed first to reuse child summaries,
11044+
// but an uninstantiated source module is itself a root. Its internal loop
11045+
// remains an error even when another unrelated root is also present.
11046+
assert_comb_loop(
11047+
"an uninstantiated module retains its internal loop diagnostic",
11048+
r#"
11049+
module Uninstantiated (
11050+
x: output logic,
11051+
y: output logic,
11052+
) {
11053+
assign x = y;
11054+
assign y = x;
11055+
}
11056+
module Top (
11057+
o: output logic,
11058+
) {
11059+
assign o = 0;
11060+
}
11061+
"#,
11062+
true,
11063+
);
11064+
}
11065+
1099511066
#[test]
1099611067
fn comb_loop_short_circuits_instance_actual_side_effects() {
1099711068
// Why this case exists: IEEE 1800-2023 11.4.11 evaluates only the selected
@@ -13413,10 +13484,11 @@ fn comb_loop_incomplete_effect_does_not_erase_proven_edges() {
1341313484

1341413485
#[test]
1341513486
fn comb_loop_child_definition_has_one_diagnostic_across_instances() {
13416-
// Why this case exists: a child graph is checked once as a declaration and
13417-
// again while each parent specialization requests its causal summary. An
13418-
// internal loop belongs to the child source definition, so repeated
13419-
// instantiation must not repeat the same source diagnostic.
13487+
// Why this case exists: parent-first analysis checks a concrete child and
13488+
// derives its causal summary in one graph build. Repeated instances must
13489+
// reuse that result, while the child's port-independent internal loop is
13490+
// still diagnosed exactly once rather than being lost with the standalone
13491+
// source-template pass.
1342013492
let detailed = analyze_comb_detailed(
1342113493
r#"
1342213494
module Child (

0 commit comments

Comments
 (0)