# UNDF: UNDF-2026-000000225 --- a/rules/group.go +++ b/rules/group.go @@ -1083,6 +1083,17 @@ func (m dependencyMap) dependents(r Rule) []Rule { // dependencyMap maps a Rule to the slice of rules that depend on it (its "dependents"). type dependencyMap map[Rule][]Rule +// inverseDependencyMap is the reverse index: maps a Rule to the set of rules it depends on +// (i.e. its "dependencies"). Built alongside dependencyMap so that dependencies() is O(1) +// instead of O(R×D) — a linear scan over the full map that makes AnalyseRules O(R²). +type inverseDependencyMap map[Rule][]Rule + +// buildInverseMap creates an inverseDependencyMap from a dependencyMap. +// Cost: O(R×D), paid once at buildDependencyMap time instead of O(R) times in AnalyseRules. +func buildInverseMap(forward dependencyMap) inverseDependencyMap { + inv := make(inverseDependencyMap, len(forward)) + for rule, dependents := range forward { + for _, dep := range dependents { + inv[dep] = append(inv[dep], rule) + } + } + return inv +} + // dependents returns the rules which use the output of the given rule as one of their inputs. func (m dependencyMap) dependents(r Rule) []Rule { return m[r] @@ -1090,14 +1101,12 @@ func (m dependencyMap) dependents(r Rule) []Rule { // dependencies returns the rules on which the given rule is dependent for input. -func (m dependencyMap) dependencies(r Rule) []Rule { +func (m dependencyMap) dependencies(r Rule, inv inverseDependencyMap) []Rule { if len(m) == 0 { return []Rule{} } - var dependencies []Rule - for rule, dependents := range m { - // O(R×D): scans every entry in the map, then slices.Contains on dependents. - if slices.Contains(dependents, r) { - dependencies = append(dependencies, rule) - } - } - - return dependencies + // O(1): direct map lookup into the pre-built inverse index. + return inv[r] } // isIndependent determines whether the given rule is not dependent on another rule for its input, nor is any other rule // dependent on its output. -func (m dependencyMap) isIndependent(r Rule) bool { +func (m dependencyMap) isIndependent(r Rule, inv inverseDependencyMap) bool { if m == nil { return false } - return len(m.dependents(r)) == 0 && len(m.dependencies(r)) == 0 + return len(m.dependents(r)) == 0 && len(m.dependencies(r, inv)) == 0 } --- a/rules/manager.go +++ b/rules/manager.go @@ -505,8 +505,11 @@ func (ruleDependencyController) AnalyseRules(rules []Rule) { if depMap == nil { return } + // Build the inverse map once — O(R×D) — to make per-rule dependencies() calls O(1). + inv := buildInverseMap(depMap) + for _, r := range rules { r.SetDependentRules(depMap.dependents(r)) - r.SetDependencyRules(depMap.dependencies(r)) + r.SetDependencyRules(depMap.dependencies(r, inv)) } }