Skip to content

Commit ae6a0e6

Browse files
committed
Add conversion of tree rules to a ROBDD
Tree-based endpoint rules can be converted to a reduced ordered binary decision diagram, or ROBDD, allowing for a much more compact representation of endpoints and more optimal evaluation with no duplicated conditions for any given path.
1 parent 11af1f9 commit ae6a0e6

File tree

8 files changed

+1107
-16
lines changed

8 files changed

+1107
-16
lines changed

smithy-rules-engine/build.gradle.kts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,6 @@ dependencies {
1515
api(project(":smithy-model"))
1616
api(project(":smithy-utils"))
1717
api(project(":smithy-jmespath"))
18+
19+
testImplementation(project(":smithy-aws-endpoints"))
1820
}
Lines changed: 279 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,279 @@
1+
/*
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
package software.amazon.smithy.rulesengine.analysis;
7+
8+
import java.util.ArrayList;
9+
import java.util.Collection;
10+
import java.util.Collections;
11+
import java.util.Comparator;
12+
import java.util.HashMap;
13+
import java.util.LinkedHashSet;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.Objects;
17+
import java.util.Set;
18+
import java.util.TreeSet;
19+
import software.amazon.smithy.rulesengine.language.EndpointRuleSet;
20+
import software.amazon.smithy.rulesengine.language.syntax.bdd.RulesBdd;
21+
import software.amazon.smithy.rulesengine.language.syntax.bdd.RulesBddCondition;
22+
import software.amazon.smithy.rulesengine.language.syntax.rule.Condition;
23+
import software.amazon.smithy.rulesengine.language.syntax.rule.EndpointRule;
24+
import software.amazon.smithy.rulesengine.language.syntax.rule.ErrorRule;
25+
import software.amazon.smithy.rulesengine.language.syntax.rule.Rule;
26+
import software.amazon.smithy.rulesengine.language.syntax.rule.TreeRule;
27+
28+
/**
29+
* Converts a {@link EndpointRuleSet} into a list of unique paths, a tree of conditions and leaves, and a BDD.
30+
*/
31+
public final class HashConsGraph {
32+
33+
// Endpoint ruleset to optimize.
34+
private final EndpointRuleSet ruleSet;
35+
36+
// Provides a hash of endpoints/errors to their index.
37+
private final Map<Rule, Integer> resultHashCons = new HashMap<>();
38+
39+
// Provides a hash of conditions to their index.
40+
private final Map<Condition, Integer> conditionHashCons = new HashMap<>();
41+
42+
// Provides a mapping of originally defined conditions to their canonicalized conditions.
43+
// (e.g., moving variables before literals in commutative functions).
44+
private final Map<Condition, Condition> canonicalizedConditions = new HashMap<>();
45+
46+
// A flattened list of unique leaves.
47+
private final List<Rule> results = new ArrayList<>();
48+
49+
// A flattened list of unique conditions
50+
private final List<RulesBddCondition> conditions = new ArrayList<>();
51+
52+
// A flattened set of unique condition paths to leaves, sorted based on desired complexity order.
53+
private final Set<BddPath> paths = new LinkedHashSet<>();
54+
55+
public HashConsGraph(EndpointRuleSet ruleSet) {
56+
this.ruleSet = ruleSet;
57+
hashConsConditions();
58+
59+
// Now build up paths and refer to the hash-consed conditions.
60+
for (Rule rule : ruleSet.getRules()) {
61+
crawlRules(rule, new LinkedHashSet<>());
62+
}
63+
}
64+
65+
// First create a global ordering of conditions. The ordering of conditions is the primary way to influence
66+
// the resulting node tables of a BDD.
67+
// 1. Simplest conditions come first (e.g., isset, booleanEquals, etc.). We build this up by gathering all
68+
// the stateless conditions and sorting them by complexity order so that simplest checks happen earlier.
69+
// 2. Stateful conditions come after, and they must appear in a dependency ordering (i.e., if a condition
70+
// depends on a previous condition to bind a variable, then it must come after its dependency). This is
71+
// done by iterating over paths and add stateful conditions, in path order, to a LinkedHashSet of
72+
// conditions, giving us a hash-consed but ordered set of all stateful conditions across all paths.
73+
private void hashConsConditions() {
74+
Set<RulesBddCondition> statelessCondition = new LinkedHashSet<>();
75+
Set<RulesBddCondition> statefulConditions = new LinkedHashSet<>();
76+
for (Rule rule : ruleSet.getRules()) {
77+
crawlConditions(rule, statelessCondition, statefulConditions);
78+
}
79+
80+
// Sort the stateless conditions by complexity order, maintaining insertion order when equal.
81+
List<RulesBddCondition> sortedStatelessConditions = new ArrayList<>(statelessCondition);
82+
sortedStatelessConditions.sort(Comparator.comparingInt(RulesBddCondition::getComplexity));
83+
84+
// Now build up the hash-consed map of conditions to their integer position in a sorted array of RuleCondition.
85+
hashConsCollectedConditions(sortedStatelessConditions);
86+
hashConsCollectedConditions(statefulConditions);
87+
}
88+
89+
private void hashConsCollectedConditions(Collection<RulesBddCondition> ruleConditions) {
90+
for (RulesBddCondition ruleCondition : ruleConditions) {
91+
conditionHashCons.put(ruleCondition.getCondition(), conditions.size());
92+
conditions.add(ruleCondition);
93+
}
94+
}
95+
96+
public List<BddPath> getPaths() {
97+
return new ArrayList<>(paths);
98+
}
99+
100+
public List<RulesBddCondition> getConditions() {
101+
return new ArrayList<>(conditions);
102+
}
103+
104+
public List<Rule> getResults() {
105+
return new ArrayList<>(results);
106+
}
107+
108+
public EndpointRuleSet getRuleSet() {
109+
return ruleSet;
110+
}
111+
112+
public RulesBdd getBdd() {
113+
return RulesBdd.from(this);
114+
}
115+
116+
// Crawl rules to build up the global total ordering of variables.
117+
private void crawlConditions(
118+
Rule rule,
119+
Set<RulesBddCondition> statelessConditions,
120+
Set<RulesBddCondition> statefulConditions
121+
) {
122+
for (Condition condition : rule.getConditions()) {
123+
if (!canonicalizedConditions.containsKey(condition)) {
124+
// Create the RuleCondition and also canonicalize the underlying condition.
125+
RulesBddCondition ruleCondition = RulesBddCondition.from(condition, ruleSet);
126+
// Add a mapping between the original condition and the canonicalized condition.
127+
canonicalizedConditions.put(condition, ruleCondition.getCondition());
128+
if (ruleCondition.isStateful()) {
129+
statefulConditions.add(ruleCondition);
130+
} else {
131+
statelessConditions.add(ruleCondition);
132+
}
133+
}
134+
}
135+
136+
if (rule instanceof TreeRule) {
137+
TreeRule treeRule = (TreeRule) rule;
138+
for (Rule subRule : treeRule.getRules()) {
139+
crawlConditions(subRule, statelessConditions, statefulConditions);
140+
}
141+
}
142+
}
143+
144+
private void crawlRules(Rule rule, Set<Integer> conditionIndices) {
145+
for (Condition condition : rule.getConditions()) {
146+
Condition c = Objects.requireNonNull(canonicalizedConditions.get(condition), "Condition not found");
147+
Integer idx = Objects.requireNonNull(conditionHashCons.get(c), "Condition not hashed");
148+
conditionIndices.add(idx);
149+
}
150+
151+
Rule leaf = null;
152+
if (rule instanceof TreeRule) {
153+
TreeRule treeRule = (TreeRule) rule;
154+
for (Rule subRule : treeRule.getRules()) {
155+
crawlRules(subRule, new LinkedHashSet<>(conditionIndices));
156+
}
157+
} else if (!rule.getConditions().isEmpty()) {
158+
leaf = createStandaloneResult(rule);
159+
} else {
160+
leaf = rule;
161+
}
162+
163+
if (leaf != null) {
164+
int position = resultHashCons.computeIfAbsent(leaf, l -> {
165+
results.add(l);
166+
return results.size() - 1;
167+
});
168+
paths.add(createPath(position, conditionIndices));
169+
}
170+
}
171+
172+
// Create a rule that strips off conditions and is just left with docs + the error or endpoint.
173+
private static Rule createStandaloneResult(Rule rule) {
174+
if (rule instanceof ErrorRule) {
175+
ErrorRule e = (ErrorRule) rule;
176+
return new ErrorRule(
177+
ErrorRule.builder().description(e.getDocumentation().orElse(null)),
178+
e.getError());
179+
} else if (rule instanceof EndpointRule) {
180+
EndpointRule e = (EndpointRule) rule;
181+
return new EndpointRule(
182+
EndpointRule.builder().description(e.getDocumentation().orElse(null)),
183+
e.getEndpoint());
184+
} else {
185+
throw new UnsupportedOperationException("Unsupported result node: " + rule);
186+
}
187+
}
188+
189+
private BddPath createPath(int leafIdx, Set<Integer> conditionIndices) {
190+
Set<Integer> statefulConditions = new LinkedHashSet<>();
191+
Set<Integer> statelessConditions = new TreeSet<>((a, b) -> {
192+
int conditionComparison = ruleComparator(conditions.get(a), conditions.get(b));
193+
// fall back to index comparison to ensure uniqueness
194+
return conditionComparison != 0 ? conditionComparison : Integer.compare(a, b);
195+
});
196+
197+
for (Integer conditionIdx : conditionIndices) {
198+
RulesBddCondition node = conditions.get(conditionIdx);
199+
if (!node.isStateful()) {
200+
statelessConditions.add(conditionIdx);
201+
} else {
202+
statefulConditions.add(conditionIdx);
203+
}
204+
}
205+
206+
return new BddPath(leafIdx, statelessConditions, statefulConditions);
207+
}
208+
209+
private int ruleComparator(RulesBddCondition a, RulesBddCondition b) {
210+
return Integer.compare(a.getComplexity(), b.getComplexity());
211+
}
212+
213+
/**
214+
* Represents a path through rule conditions to reach a specific result.
215+
*
216+
* <p>Contains both stateless conditions (sorted by complexity) and stateful conditions (ordered by dependency)
217+
* that must be evaluated to reach the target leaf (endpoint or error).
218+
*/
219+
public static final class BddPath {
220+
221+
// The endpoint or error index.
222+
private final int leafIndex;
223+
224+
// Conditions that create or use stateful bound variables and must be maintained in order.
225+
private final Set<Integer> statefulConditions;
226+
227+
// Sort conditions based on complexity scores.
228+
private final Set<Integer> statelessConditions;
229+
230+
private int hash;
231+
232+
BddPath(int leafIndex, Set<Integer> statelessConditions, Set<Integer> statefulConditions) {
233+
this.leafIndex = leafIndex;
234+
this.statelessConditions = Collections.unmodifiableSet(statelessConditions);
235+
this.statefulConditions = Collections.unmodifiableSet(statefulConditions);
236+
}
237+
238+
public Set<Integer> getStatefulConditions() {
239+
return statefulConditions;
240+
}
241+
242+
public Set<Integer> getStatelessConditions() {
243+
return statelessConditions;
244+
}
245+
246+
public int getLeafIndex() {
247+
return leafIndex;
248+
}
249+
250+
@Override
251+
public boolean equals(Object object) {
252+
if (this == object) {
253+
return true;
254+
} else if (object == null || getClass() != object.getClass()) {
255+
return false;
256+
}
257+
BddPath path = (BddPath) object;
258+
return leafIndex == path.leafIndex
259+
&& statefulConditions.equals(path.statefulConditions)
260+
&& statelessConditions.equals(path.statelessConditions);
261+
}
262+
263+
@Override
264+
public int hashCode() {
265+
int result = hash;
266+
if (result == 0) {
267+
result = Objects.hash(leafIndex, statefulConditions, statelessConditions);
268+
hash = result;
269+
}
270+
return result;
271+
}
272+
273+
@Override
274+
public String toString() {
275+
return "Path{statelessConditions=" + statelessConditions + ", statefulConditions=" + statefulConditions
276+
+ ", leafIndex=" + leafIndex + '}';
277+
}
278+
}
279+
}

0 commit comments

Comments
 (0)