/* * Copyright 2003-2015 JetBrains s.r.o. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package jetbrains.mps.generator.impl.plan; import jetbrains.mps.generator.runtime.TemplateMappingConfiguration; import jetbrains.mps.project.structure.modules.mappingpriorities.MappingPriorityRule; import jetbrains.mps.project.structure.modules.mappingpriorities.RuleType; import jetbrains.mps.util.containers.MultiMap; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Set; /** * Graph of Groups, where each group constitutes one or more map configurations. * * Invariant, once #finalizeEdges has been invoked: * foreach entry1 in myRulePriorityEntries { * Exists entry2 in myRulePriorityEntries : entry1.sooner == entry2.later * } * In other words, for rules A < C, B < C we don't keep single edge AB < C, but two distinct edges. * Groups are merged when there's 'strictly together' relation between them, and all rules including parts * of the merged group are updated to use this new merged node. * I.e. with A == B rule, AB < C (and, if no other rule present, 0 < AB) would replace both A < C and B < C rules. * Dependencies outside of 'strictly together' group are kept as separate edges, i.e. B = D would yield * A < C, BD < C (and, perhaps, 0 < BD) rules, not ABD < C or {A, BD} < C * * As a most notable consequence, Group.includes and Group.hasCommonMappings() shall be used exclusively * when dealing with incomplete coherent groups. * @author Artem Tikhomirov */ class PriorityGraph { // graph edges private final List<Entry> myRulePriorityEntries; private final Set<TemplateMappingConfiguration> myNonTrivialEdges; public PriorityGraph() { myRulePriorityEntries = new LinkedList<Entry>(); myNonTrivialEdges = new HashSet<TemplateMappingConfiguration>(); } public void addEdge(TemplateMappingConfiguration tmc, Collection<TemplateMappingConfiguration> appliedSooner, MappingPriorityRule rule) { boolean isStrict = rule.getType() == RuleType.STRICTLY_BEFORE || rule.getType() == RuleType.STRICTLY_AFTER; final Group lowPriorityGroup = new Group(tmc); final Set<MappingPriorityRule> ruleSet = Collections.singleton(rule); for (TemplateMappingConfiguration sooner : appliedSooner) { myRulePriorityEntries.add(new Entry(new Group(sooner), lowPriorityGroup, isStrict, ruleSet)); } myNonTrivialEdges.add(tmc); } public void finalizeEdges(Collection<TemplateMappingConfiguration> allMapConfigurations) { HashSet<TemplateMappingConfiguration> trivialEdges = new HashSet<TemplateMappingConfiguration>(allMapConfigurations); trivialEdges.removeAll(myNonTrivialEdges); for (TemplateMappingConfiguration tmc : trivialEdges) { myRulePriorityEntries.add(newTrivialEdge(new Group(tmc))); } } public void replaceWeakEdgesWithStrict() { // inv: !weakEntry.isStrict && !weakEntry.isTrivial final ArrayDeque<Entry> weakEntries = new ArrayDeque<Entry>(); for (Entry entry : myRulePriorityEntries) { if (!entry.isStrict() && !entry.isTrivial()) { weakEntries.add(entry); } } if (Boolean.TRUE.booleanValue()) { for (Entry weak : weakEntries) { weak.makeStrict(); } return; } /* sooner later * A <= B * B < C * B <= D * X < A * Y <= A */ while (!weakEntries.isEmpty()) { Entry weak = weakEntries.removeFirst(); myRulePriorityEntries.remove(weak); // weak edge will be replaced with new edges (either strong or weak) Collection<Entry> toAdd = new ArrayList<Entry>(); for (Entry entry : myRulePriorityEntries) { if (entry.isTrivial()) { // trivial edges are there just for graph completeness, and should not take part in transformations continue; } // entry dependency may be substituted for weak dependency final boolean substituteForSooner = entry.later().equals(weak.sooner()); // entry depends on weak dependency final boolean dependsOnWeak = entry.sooner().equals(weak.later()); if (!substituteForSooner && !dependsOnWeak) { continue; } final Entry newEntry; HashSet<MappingPriorityRule> mergedRules = new HashSet<MappingPriorityRule>(entry.getRules()); mergedRules.addAll(weak.getRules()); if (substituteForSooner) { // A <= B; X < A, Y <= A --> add rules X < B, Y <= B newEntry = new Entry(entry.sooner(), weak.later(), entry.isStrict(), mergedRules); } else { assert dependsOnWeak; // A <= B; B < C, B <= D --> add rules A < C, A <= D newEntry = new Entry(weak.sooner(), entry.later(), entry.isStrict(), mergedRules); } toAdd.add(newEntry); if (!newEntry.isStrict() && !newEntry.isTrivial()) { weakEntries.add(newEntry); // update queue with new edge to handle } } if (toAdd.isEmpty()) { // if there's no change // neither lhs nor rhs of the weak edge is part of any other edge, it's safe to replace it with strict toAdd.add(weak.makeStrict()); // A <= B without any dependant rules effectively means A < B } else { /* Start: A <= B, X < A, B < Y * toAdd: X < B, A < Y * Decision to make: if there's relation between X and Y, we're all set * if not, we need a rule to tell A 'not later' B */ // What new lhs and rhs groups did we get? HashSet<Group> addedSooner = new HashSet<Group>(); HashSet<Group> addedLater = new HashSet<Group>(); for (Entry e : toAdd) { addedSooner.add(e.sooner()); addedLater.add(e.later()); } addedSooner.remove(weak.sooner()); addedLater.remove(weak.later()); /* * If there are existing edges X < Z and Z < Y, we build a closure for addedSooner X={Z,Y} * to see it intersects with addedLater. * If any element in rhs appears as 'after' of any element in lhs in existing rules, * then this ensures weak dependency being processed is kept. If elements in rhs * are not in any relation with lhs elements, then we need explicit edge to record 'not later' knowledge * of the current weak edge. */ TransitiveClosure<Group> closureBuilder = new TransitiveClosure<Group>(); for (Entry e : myRulePriorityEntries) { if (!e.isTrivial()) { closureBuilder.feed(e.sooner(), e.later()); } } // all elements to show up later than those we've added as 'sooner' for our weak.later() // iow, 'not later' than weak.later() HashSet<Group> closure = new HashSet<Group>(); for (Group l : addedSooner) { closure.addAll(closureBuilder.closure(l)); } // see if newly added later (than weak.sooner) elements are in relation with the closure closure.retainAll(addedLater); if (closure.isEmpty()) { // nope, can't get from addedSooner to addedLater toAdd.add(weak.makeStrict()); } } myRulePriorityEntries.addAll(toAdd); } } // pre: groups in coherentMappings do not intersect public void updateWithCoherent(List<Group> coherentMappings, PriorityConflicts conflicts) { // if any of 'coherent' mappings happens before another group, make this group dependant from all coherent mappings. // if there's no mapping that establish relation for coherent mapping (i.e. only 'trivial' mappings), replace these trivial mappings with single // one with the coherent group Collection<Entry> toRemove = new HashSet<Entry>(); for (Group g : coherentMappings) { Collection<Entry> hiPriCoherentToAdd = new HashSet<Entry>(); Collection<Entry> loPriCoherentToAdd = new HashSet<Entry>(); boolean coherentGroupNeedsTrivialEdge = true; for (Entry entry : myRulePriorityEntries) { final boolean soonerMatches = g.includes(entry.sooner()); final boolean laterMatches = g.includes(entry.later()); if (!soonerMatches && !laterMatches) { continue; } if (soonerMatches && laterMatches) { if (entry.isStrict()) { // same TMC on both sides of the strict rule conflicts.registerCoherentWithStrict(g, entry.sooner(), entry.getRules()); } toRemove.add(entry); // no reason to keep AB <= AB entry; // if there would be no other edge with coherent group, coherentGroupNeedsTrivialEdge ensures coherent group doesn't get lost continue; } toRemove.add(entry); if (soonerMatches) { // coherent group matches sooner/hi-pri side // introduce a new edge, from entry's later to coherent group hiPriCoherentToAdd.add(new Entry(g, entry.later(), entry.isStrict(), entry.getRules())); } if (laterMatches) { // coherent group matches low-pri side. // There's little value replacing 'element of coherent'->empty. with 'coherent group'->empty, unless there are no other rules. // I use coherentGroupNeedsTrivialEdge to track if there's an entry 'coherent'->non-empty if (!entry.isTrivial()) { loPriCoherentToAdd.add(new Entry(entry.sooner(), g, entry.isStrict(), entry.getRules())); coherentGroupNeedsTrivialEdge = false; } } } HashSet<Entry> toAdd = new HashSet<Entry>(); // Remove duplicates, A<X, B<X, C<X, {ABC} is replaced with single {ABC} < X instead of 3 equivalent edges MultiMap<Group, Entry> groupByLater = new MultiMap<Group, Entry>(); for (Entry e : hiPriCoherentToAdd) { assert e.sooner().equals(g); groupByLater.putValue(e.later(), e); } for (Group loPri : groupByLater.keySet()) { Set<MappingPriorityRule> involvedRules = new HashSet<MappingPriorityRule>(); boolean atLeastOneStrict = false; // A < X, B <= X, {AB} - strict edge if there's at least 1 strict edge for (Entry e : groupByLater.get(loPri)) { involvedRules.addAll(e.getRules()); atLeastOneStrict |= e.isStrict(); } toAdd.add(new Entry(g, loPri, atLeastOneStrict, involvedRules)); } // Remove duplicates, X<A, X<B, X<C, {ABC} is replaced with single X < {ABC} instead of 3 equivalent edges MultiMap<Group, Entry> groupBySooner = new MultiMap<Group, Entry>(); for (Entry e : loPriCoherentToAdd) { assert e.later().equals(g); groupBySooner.putValue(e.sooner(), e); } for (Group hiPri : groupBySooner.keySet()) { Set<MappingPriorityRule> involvedRules = new HashSet<MappingPriorityRule>(); boolean atLeastOneStrict = false; // X < A, X <= B, {AB} - strict edge if there's at least 1 strict edge for (Entry e : groupBySooner.get(hiPri)) { involvedRules.addAll(e.getRules()); atLeastOneStrict |= e.isStrict(); } toAdd.add(new Entry(hiPri, g, atLeastOneStrict, involvedRules)); } // // if (coherentGroupNeedsTrivialEdge) { toAdd.add(newTrivialEdge(g)); } myRulePriorityEntries.addAll(toAdd); myRulePriorityEntries.removeAll(toRemove); toRemove.clear(); } } public Collection<Group> getGroupsNotInDependency() { HashSet<Group> rv = new HashSet<Group>(); // all groups that appear at 'sooner' side of rules HashSet<Group> allSoonerGroups = new HashSet<Group>(myRulePriorityEntries.size() * 2); // there might be multiple dependency edges from a single node, no need to check same node more than once HashSet<Group> uniqueLaterGroups = new HashSet<Group>(myRulePriorityEntries.size() * 2); for (Entry e : myRulePriorityEntries) { if (!e.isTrivial()) { allSoonerGroups.add(e.sooner()); } uniqueLaterGroups.add(e.later()); } for (Group candidate : uniqueLaterGroups) { if (!allSoonerGroups.contains(candidate)) { rv.add(candidate); } } return rv; } public void dropEdgesOf(Collection<Group> groups) { for (Iterator<Entry> it = myRulePriorityEntries.iterator(); it.hasNext(); ) { if (groups.contains(it.next().later())) { it.remove(); } } for (Entry entry : myRulePriorityEntries) { if (groups.contains(entry.sooner())) { entry.makeTrivial(); } } } // XXX next methods (including, but not limited to those with PriorityConflicts) cry for edge iterator // (either external or internal), so that we can decouple graph and alg impl from error checking void checkSelfLocking(PriorityConflicts conflicts) { for (Entry edge : myRulePriorityEntries) { if (edge.sooner().equals(edge.later())) { if (edge.isStrict()) { conflicts.registerSelfLock(edge.sooner(), edge.later(), edge.getRules()); } // remove self-lock edge.makeTrivial(); } } } void checkLowPrioLocksTopPrio(PriorityConflicts conflicts) { ArrayList<Entry> toDrop = new ArrayList<Entry>(); for (Entry edge : myRulePriorityEntries) { if (edge.isTrivial()) { continue; } if (edge.later().isTopPriority() && !edge.sooner().isTopPriority()) { conflicts.registerLoPriLocksHiPri(edge.sooner(), edge.later(), edge.getRules()); toDrop.add(edge); } } myRulePriorityEntries.removeAll(toDrop); } /** * Cycle of weak rules A <= B, B <= C, C <= A is transformed into a single 'same step' group {ABC}. */ Collection<Group> removeWeakCycles() { CycleDetector cd = new CycleDetector(); for (Entry edge : myRulePriorityEntries) { if (edge.isTrivial() || edge.isStrict()) { continue; } cd.feed(edge); } ArrayList<Group> rv = new ArrayList<Group>(); HashSet<Entry> toDrop = new HashSet<Entry>(); Collection<Cycle> cycles = cd.detect(); for (Cycle c : cycles) { rv.add(new Group(c.elements)); toDrop.addAll(c.edges); } myRulePriorityEntries.removeAll(toDrop); return rv; } void reportEdgesLeft(PriorityConflicts conflicts) { CycleDetector cd = new CycleDetector(); HashSet<MappingPriorityRule> rules = new HashSet<MappingPriorityRule>(); for (Entry edge : myRulePriorityEntries) { if (edge.isTrivial()) { continue; } cd.feed(edge); rules.addAll(edge.getRules()); } conflicts.registerLeftovers(rules); for (Cycle c : cd.detect()) { conflicts.registerCycle(c); } } public boolean isEmpty() { return myRulePriorityEntries.isEmpty(); } public void dump() { for (Entry entry : myRulePriorityEntries) { System.out.println(entry); } } private static Entry newTrivialEdge(Group g) { return new Entry(new Group(), g, false, Collections.<MappingPriorityRule>emptyList()); } // Edge of dependency graph private static class Entry { private Group myLaterGroup; private Group mySoonerGroup; private boolean myStrict; // rules this relation originates from private final Set<MappingPriorityRule> myRules; public Entry(Group highPriorityGroup, Group lowPriorityGroup, boolean strict, Collection<MappingPriorityRule> rules) { myLaterGroup = lowPriorityGroup; mySoonerGroup = highPriorityGroup; myStrict = strict; myRules = new HashSet<MappingPriorityRule>(rules); } public Group later() { return myLaterGroup; } public Group sooner() { return mySoonerGroup; } public Set<MappingPriorityRule> getRules() { return myRules; } public boolean isStrict() { return myStrict; } // Trivial edge is auxiliary, merely a mechanism to complete graph and to report all the vertices during topological sorting public boolean isTrivial() { return mySoonerGroup.isEmpty(); } public void makeTrivial() { mySoonerGroup = new Group(); } public Entry makeStrict() { myStrict = true; return this; } @Override public String toString() { return String.format("%s %s %s", mySoonerGroup, isStrict() ? '<' : '\u2264', myLaterGroup); } } static class CycleDetector { private MultiMap<Group, Entry> soonerToEntry = new MultiMap<Group, Entry>(); private TransitiveClosure<Group> soonerToLater = new TransitiveClosure<Group>(); public void feed(Entry edge) { soonerToEntry.putValue(edge.sooner(), edge); soonerToLater.feed(edge.sooner(), edge.later()); } Collection<Cycle> detect() { HashSet<Cycle> rv = new HashSet<Cycle>(); for (Group g : soonerToEntry.keySet()) { // build closure of all possible rhs (later) elements // i.e. for A <= B, B <= C, C <= D, A <= X and given A, builds A := B, C, D, X Set<Group> rhsClosure = soonerToLater.closure(g); if (!rhsClosure.contains(g)) { continue; } HashSet<Group> actualCycleParticipants = new HashSet<Group>(); HashSet<Entry> toDrop = new HashSet<Entry>(); for (Group cycleElementCandidate : rhsClosure) { boolean isActualCycleElement = false; // element in the rhsClosure not necessarily part of the cycle, // e.g. A <= B, B <= A, A <= C, rhcClosure is B,C,A, but C is not in the cycle for (Entry edge : soonerToEntry.get(cycleElementCandidate)) { assert rhsClosure.contains(edge.sooner()); // that's the way we've built soonerToEntry if (rhsClosure.contains(edge.later())) { // both sides of the rule are in rhsClosure, edge is part of the cycle then toDrop.add(edge); isActualCycleElement = true; } } if (isActualCycleElement) { actualCycleParticipants.add(cycleElementCandidate); } } assert !actualCycleParticipants.isEmpty(); // rhsClosure.contains(g) ensures there's indeed a cycle (at least one-element, A <= A) rv.add(new Cycle(actualCycleParticipants, toDrop)); } return rv; } } static class Cycle { public final Set<Group> elements; // all graph nodes that build up a cycle public final Set<Entry> edges; // edges involved into the cycle public Cycle(Set<Group> elements, Set<Entry> edges) { this.elements = elements; this.edges = edges; } // Rules involved in cycle inception public Collection<MappingPriorityRule> getRules() { HashSet<MappingPriorityRule> rv = new HashSet<MappingPriorityRule>(); for (Entry edge : edges) { rv.addAll(edge.getRules()); } return rv; } @Override public int hashCode() { return elements.hashCode() + edges.hashCode(); } @Override public boolean equals(Object obj) { if (!(obj instanceof Cycle)) { return false; } Cycle o = ((Cycle) obj); return elements.equals(o.elements) && edges.equals(o.edges); } } /** * For a transitive relation, builds a closure of elements. The closure doesn't contain the starting one, unless there's a cycle. * E.g. fed with elements: AxB, BxC, CxD, produces for A: {B,C,D}, for C: {D} * With another element, DxA, produces for A: {B,C,D,A}, for C:{D,A,B,C} */ private static class TransitiveClosure<T> { private final MultiMap<T,T> myMap = new MultiMap<T, T>(); public void feed(T left, T right) { myMap.putValue(left, right); } public Set<T> closure(T element) { HashSet<T> rhsClosure = new HashSet<T>(); ArrayDeque<T> rhsQueue = new ArrayDeque<T>(); rhsQueue.addAll(myMap.get(element)); while (!rhsQueue.isEmpty()) { T rhs = rhsQueue.removeFirst(); if (rhsClosure.add(rhs)) { rhsQueue.addAll(myMap.get(rhs)); } } return rhsClosure; } } }