|
| 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