/* * 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.template; import jetbrains.mps.generator.impl.TemplateGenerator; import jetbrains.mps.generator.impl.reference.PostponedReference; import jetbrains.mps.generator.impl.reference.ReferenceInfo; import jetbrains.mps.generator.impl.reference.ReferenceInfo_CopiedInputNode; import jetbrains.mps.smodel.FastNodeFinderManager; import jetbrains.mps.smodel.StaticReference; import jetbrains.mps.smodel.nodeidmap.INodeIdToNodeMap; import jetbrains.mps.smodel.nodeidmap.UniversalOptimizedNodeIdMap; import jetbrains.mps.util.SNodeOperations; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SContainmentLink; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SModelReference; import org.jetbrains.mps.openapi.model.SNode; import org.jetbrains.mps.openapi.model.SNodeId; import org.jetbrains.mps.openapi.model.SNodeUtil; import org.jetbrains.mps.openapi.model.SReference; import org.jetbrains.mps.openapi.util.TreeIterator; import org.jetbrains.mps.util.DescendantsTreeIterator; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Set; /** * Collect changes during template processing * @author Artem Tikhomirov */ public abstract class DeltaBuilder { private final List<DeltaRoot> myDelta = new ArrayList<DeltaRoot>(); private final List<ReplacedRoot> myReplacedRoots = new ArrayList<ReplacedRoot>(); // view: myDelta.select(ReplacedRoot) private final List<NewRoot> myNewRoots = new ArrayList<NewRoot>(); // view: myDelta.select(NewRoot) private final List<CopyRoot> myCopyRoots; // view: myDelta.select(CopyRoot) private final List<DeletedRoot> myDeletedRoots = new ArrayList<DeletedRoot>(); // view: myDelta.select(DeletedRoot) private final UniversalOptimizedNodeIdMap myNewNodes = new UniversalOptimizedNodeIdMap(); protected DeltaBuilder(List<CopyRoot> rootsStorage) { myCopyRoots = rootsStorage; } public static DeltaBuilder newSingleThreadDeltaBuilder() { return new DeltaBuilder(new ArrayList<CopyRoot>()) { private final Deque<SNode> myNestedCopyRoots = new ArrayDeque<SNode>(); private CopyRoot myCurrentRoot; private final List<SubTree> myCurrentFragments = new ArrayList<SubTree>(); @Override protected Deque<SNode> getNestedCopyRoots() { return myNestedCopyRoots; } @Override protected CopyRoot getCurrentRoot() { return myCurrentRoot; } @Override protected void setCurrentRoot(CopyRoot currentRoot) { myCurrentRoot = currentRoot; } @Override protected List<SubTree> getCurrentFragments() { return myCurrentFragments; } @Override protected void initCurrentFragments() { // no-op } @Override protected void clearCurrentFragments() { myCurrentFragments.clear(); } }; } public static DeltaBuilder newConcurrentDeltaBuilder() { return new DeltaBuilder(Collections.synchronizedList(new ArrayList<CopyRoot>())) { private final ThreadLocal<CopyRoot> myCurrentRoot = new ThreadLocal<CopyRoot>(); private final ThreadLocal<Deque<SNode>> myNestedCopyRoots = new ThreadLocal<Deque<SNode>>(); private final ThreadLocal<List<SubTree>> myCurrentFragments = new ThreadLocal<List<SubTree>>(); @Override protected Deque<SNode> getNestedCopyRoots() { Deque<SNode> ncr = myNestedCopyRoots.get(); if (ncr == null) { myNestedCopyRoots.set(ncr = new ArrayDeque<SNode>()); } return ncr; } @Override protected CopyRoot getCurrentRoot() { return myCurrentRoot.get(); } @Override protected void setCurrentRoot(CopyRoot currentRoot) { myCurrentRoot.set(currentRoot); } @Override protected List<SubTree> getCurrentFragments() { List<SubTree> l = myCurrentFragments.get(); if (l == null) { return Collections.emptyList(); } return l; } @Override protected void initCurrentFragments() { assert myCurrentFragments.get() == null; myCurrentFragments.set(new ArrayList<SubTree>()); } @Override protected void clearCurrentFragments() { assert myCurrentFragments.get() != null; myCurrentFragments.set(null); } }; } public void enterInputRoot(@NotNull SNode node) { assert getCurrentRoot() == null; assert getCurrentFragments().isEmpty(); final CopyRoot r = new CopyRoot(node); setCurrentRoot(r); initCurrentFragments(); myCopyRoots.add(r); } public void deleteInputRoot(@NotNull SNode node) { DeletedRoot r = new DeletedRoot(node); synchronized (myDelta) { // XXX synchronize here is just quick-n-dirty guard, revisit multi-threading and use of ThreadLocals myDeletedRoots.add(r); myDelta.add(r); // we don't care about order of deletions } } public void leaveInputRoot(@NotNull SNode node) { assert getCurrentRoot() != null; assert getCurrentRoot().myRoot == node; final List<SubTree> fragments = getCurrentFragments(); getCurrentRoot().mySubTrees = fragments.toArray(new SubTree[fragments.size()]); setCurrentRoot(null); clearCurrentFragments(); } public void enterNestedCopySrc(@NotNull SNode node) { if (!isInsideCopyRoot()) { return; } if (getNestedCopyRoots().isEmpty()) { assert getCurrentRoot() != null; getCurrentFragments().add(new SubTree(node)); } getNestedCopyRoots().push(node); } public void leaveNestedCopySrc(@NotNull SNode node) { if (!isInsideCopyRoot()) { return; } SNode n = getNestedCopyRoots().pop(); if (n != node) { throw new IllegalStateException(); } } /** * With parallel generation and in-place at any step, there are chances COPY_SRC from root mapping rule is executed * after TemplateGenerator.myDeltaBuilder have been initialized, interleaved with root copying. * This check is a sort of quick-n-dirty fix, as there seems to be the only place (TEE.copyNodes and eventually TemplateGenerator.copySrc) * and I don't have time to refactor TG in a way DeltaBuilder is not instance field but lives inside root copy facility * (although copy facility might be the field, and TG#copySRC might extract DeltaBuilder from it, if present) */ private boolean isInsideCopyRoot() { return getCurrentRoot() != null; } /** * @param replacedNode node in the input model to be replaced with nodes from <code>subTree</code> * @param roleInParent node's containment * @param subTree new nodes to put into model, can be empty to indicate node removal. */ public void registerSubTree(@NotNull SNode replacedNode, @NotNull SContainmentLink roleInParent, @NotNull Collection<SNode> subTree) { if (getNestedCopyRoots().isEmpty()) { assert getCurrentRoot() != null; getCurrentFragments().add(new SubTree(replacedNode, roleInParent, subTree)); } } /* * Invoked from the single thread */ public void registerRoot(@Nullable SNode oldRoot, @NotNull SNode newRoot) { if (oldRoot == null) { NewRoot r = new NewRoot(newRoot); myDelta.add(r); myNewRoots.add(r); fillNodeMap(newRoot); } else if (oldRoot == newRoot) { CopyRoot cr = null; for (CopyRoot r : myCopyRoots) { if (r.myRoot == newRoot) { cr = r; break; } } if (cr == null) { throw new IllegalStateException(); } myDelta.add(cr); cr.fillNodeMap(myNewNodes); } else { ReplacedRoot rr = null; for (ReplacedRoot r : myReplacedRoots) { if (r.myReplacedRoot == oldRoot) { rr = r; break; } } if (rr == null) { myDelta.add(rr = new ReplacedRoot(oldRoot, newRoot)); myReplacedRoots.add(rr); } else { rr.myReplacements.add(newRoot); } fillNodeMap(newRoot); } } /** * Delayed/postponed changes may replace nodes created earlier, and we shall update * delta accordingly. */ public void replacePlaceholderNode(@NotNull SNode placeholder, @NotNull SNode actual) { clearNodeMap(placeholder); fillNodeMap(actual); if (placeholder.getParent() == null) { // e.g. MAP-SRC with mapper function at root node in CreateRootRule or MapRootRule for (NewRoot r : myNewRoots) { if (r.myRoot == placeholder) { myNewRoots.remove(r); int i = myDelta.indexOf(r); myDelta.set(i, new NewRoot(actual)); return; } } for (ReplacedRoot r : myReplacedRoots) { int i = r.myReplacements.indexOf(placeholder); if (i != -1) { r.myReplacements.set(i, actual); return; } } // mapper func in MAP-SRC for top node of in-place change for (CopyRoot r : myCopyRoots) { for (SubTree t : r.mySubTrees) { if (t.isSourceCopy()) { continue; } int i = t.myReplacement.indexOf(placeholder); if (i != -1) { t.myReplacement.set(i, actual); return; } } } } else { // it's a child, go ahead and replace it. Once delta is applied, actual would get where expected. SNodeUtil.replaceWithAnother(placeholder, actual); } } void fillNodeMap(@NotNull SNode newNode) { for (SNode n : SNodeUtil.getDescendants(newNode)) { myNewNodes.put(n.getNodeId(), n); } } void clearNodeMap(@NotNull SNode newNode) { for (SNode n : SNodeUtil.getDescendants(newNode)) { myNewNodes.remove(n.getNodeId()); } } @Nullable public SNode findOutputNodeById(@NotNull SNodeId nodeId) { return myNewNodes.get(nodeId); } public boolean hasChanges() { for (DeltaRoot dr : myDelta) { if (false == dr instanceof CopyRoot) { return true; // both new and replaced root do constitute a change } if (((CopyRoot) dr).bringsChanges()) { return true; } } return false; } public void prepareReferences(SModel inputModel, TemplateGenerator generator) { HashSet<SNode> allReplacedNodes = new HashSet<SNode>(); for (CopyRoot root : myCopyRoots) { allReplacedNodes.addAll(root.getReplacedNodes()); } // reference target under deleted root needs update, too for (DeletedRoot root : myDeletedRoots) { allReplacedNodes.add(root.myRoot); } for (ReplacedRoot rr : myReplacedRoots) { allReplacedNodes.add(rr.myReplacedRoot); } // FastNodeFinder update mechanism performs poorly (badly, in fact) with massive in-place updates. // It's faster to rebuild FNF completely than to update it. E.g step 4 for lang.editor/editor // spent 90 seconds out of 105 in replace of 9k children FastNodeFinderManager.dispose(inputModel); final SModelReference inputModelRef = inputModel.getReference(); // update references between changed model elements for (CopyRoot root : myCopyRoots) { final Set<SNode> replacedNodes; replacedNodes = root.getReplacedNodes(); TreeIterator<SNode> it = root.iterateOrigin(); while (it.hasNext()) { SNode next = it.next(); if (replacedNodes.contains(next)) { // nodes under replaced already have PostponedReferences it.skipChildren(); continue; } for (SReference reference : next.getReferences()) { assert reference instanceof PostponedReference == false : "!!! unexpected PostponedReference in the input model"; if (!inputModelRef.equals(reference.getTargetSModelReference())) { continue; } final SNode referenceTarget = reference.getTargetNode(); SNode outputTarget = referenceTarget; while (outputTarget != null) { if (allReplacedNodes.contains(outputTarget)) { // reference points elsewhere in this model under a replaced node. // reference needs update, its outputTarget is among replaced nodes ReferenceInfo refInfo = new ReferenceInfo_CopiedInputNode(next, referenceTarget); new PostponedReference(reference.getLink(), reference.getSourceNode(), refInfo).registerWith(generator); break; // while outputTarget } outputTarget = outputTarget.getParent(); } } } } // make references to point to node directly, not (ModelId+NodeId) // as it would be impossible to resolve model once root is detached for (SNode rn : allReplacedNodes) { for (SNode n : SNodeUtil.getDescendants(rn)) { for (SReference r : n.getReferences()) { if (!inputModelRef.equals(r.getTargetSModelReference()) || ! (r instanceof StaticReference)) { continue; } ((StaticReference) r).makeDirect(); } } } } public void applyInplace(SModel inputModel) { // make the structure change, at last for (DeltaRoot dr : myDelta) { // additions from NewRoot and ReplacedRoot come in the order they were scheduled to be applied // not the order they were ready - to get same order in parallel gen. Although additions from replaced // come to the tail of root nodes list as there's no way to keep index of root node. if (dr instanceof NewRoot) { inputModel.addRootNode(((NewRoot) dr).myRoot); } else if (dr instanceof ReplacedRoot) { ReplacedRoot rr = (ReplacedRoot) dr; // XXX Seems there's no way to replace root node in its original position ?! inputModel.removeRootNode(rr.myReplacedRoot); for (SNode replacement : rr.myReplacements) { inputModel.addRootNode(replacement); } } else if (dr instanceof DeletedRoot) { DeletedRoot root = (DeletedRoot) dr; SModel rootModel = root.myRoot.getModel(); if (rootModel != null) { // it's possible for the root to be deleted already, e.g. when there are rootMapRules with keepSourceRoot==true and // a drop rule to clear origin root once all desired targets have been created. assert root.myRoot.getModel() == inputModel; inputModel.removeRootNode(root.myRoot); } } else { CopyRoot root = (CopyRoot) dr; // replace nodes for (SubTree tree : root.mySubTrees) { if (tree.isSourceCopy()) { continue; } assert tree.myInputNode.getModel() == inputModel; SNode inputParentNode = tree.myInputNode.getParent(); SNode anchor = tree.myInputNode.getNextSibling(); inputParentNode.removeChild(tree.myInputNode); for (SNode replacement : tree.myReplacement) { inputParentNode.insertChildBefore(tree.myRoleInParent, replacement, anchor); } } } } } @SuppressWarnings("unused") public void dump() { for (DeltaRoot dr : myDelta) { if (dr instanceof NewRoot) { System.out.printf("+%s\n", SNodeOperations.getDebugText(((NewRoot) dr).myRoot)); } else if (dr instanceof ReplacedRoot) { ReplacedRoot rr = (ReplacedRoot) dr; System.out.printf("R%s - %d\n", SNodeOperations.getDebugText(rr.myReplacedRoot), rr.myReplacements.size()); } else if (dr instanceof DeletedRoot) { System.out.printf("-%s\n", SNodeOperations.getDebugText(((DeletedRoot) dr).myRoot)); } else { CopyRoot root = (CopyRoot) dr; char c = root.mySubTrees.length > 0 ? '*' : '~'; System.out.printf("%c%s\n", c, SNodeOperations.getDebugText(root.myRoot)); for (SubTree tree : root.mySubTrees) { if (tree.isSourceCopy()) { System.out.printf(" copysrc %s\n", tree.myInputNode); } else { StringBuilder sb = new StringBuilder(); for (SNode r : tree.myReplacement) { sb.append(r.toString()); sb.append(','); } System.out.printf(" %s - %d - %s -> (%s)\n", tree.myRoleInParent, tree.myReplacement.size(), tree.myInputNode, sb); } } } } System.out.println(); } protected abstract Deque<SNode> getNestedCopyRoots(); protected abstract CopyRoot getCurrentRoot(); protected abstract void setCurrentRoot(CopyRoot currentRoot); protected abstract List<SubTree> getCurrentFragments(); protected abstract void initCurrentFragments(); protected abstract void clearCurrentFragments(); private interface DeltaRoot { } private static class NewRoot implements DeltaRoot { public final SNode myRoot; public NewRoot(@NotNull SNode newRoot) { myRoot = newRoot; } } private static class DeletedRoot implements DeltaRoot { public final SNode myRoot; public DeletedRoot(@NotNull SNode root) { myRoot = root; } } private static class ReplacedRoot implements DeltaRoot { public final SNode myReplacedRoot; public final List<SNode> myReplacements; public ReplacedRoot(@NotNull SNode oldRoot, @NotNull SNode newRoot) { myReplacedRoot = oldRoot; myReplacements = new ArrayList<SNode>(4); myReplacements.add(newRoot); } } private static class CopyRoot implements DeltaRoot { public final SNode myRoot; private SubTree[] mySubTrees; CopyRoot(SNode root) { myRoot = root; } public boolean bringsChanges() { for (SubTree tree : mySubTrees) { if (!tree.isSourceCopy()) { return true; } } return false; } /** * walk over input root */ public TreeIterator<SNode> iterateOrigin() { return new DescendantsTreeIterator(myRoot); } // get to know nodes about to be injected public void fillNodeMap(INodeIdToNodeMap map) { if (mySubTrees == null) { return; } Set<SNode> replacedNodes = getReplacedNodes(); TreeIterator<SNode> it = iterateOrigin(); while (it.hasNext()) { SNode next = it.next(); if (replacedNodes.contains(next)) { it.skipChildren(); continue; } map.put(next.getNodeId(), next); } for (SubTree t : mySubTrees) { if (t.isSourceCopy()) { continue; } t.fillNodeMap(map); } } public Set<SNode> getReplacedNodes() { if (mySubTrees == null) { return Collections.emptySet(); } HashSet<SNode> rv = new HashSet<SNode>(mySubTrees.length); for (SubTree tree : mySubTrees) { if (!tree.isSourceCopy()) { rv.add(tree.myInputNode); } } return rv; } } private static class SubTree { @NotNull private final SNode myInputNode; private final SContainmentLink myRoleInParent; private final List<SNode> myReplacement; // we need to ensure order doesn't change if we later alter new nodes (MAP-SRC replacements) public SubTree(@NotNull SNode inputNode, @NotNull SContainmentLink roleInParent, @NotNull Collection<SNode> subTree) { myInputNode = inputNode; myRoleInParent = roleInParent; myReplacement = subTree instanceof List ? (List<SNode>) subTree : new ArrayList<SNode>(subTree); } public SubTree(@NotNull SNode inputNode) { myInputNode = inputNode; myRoleInParent = null; myReplacement = null; } public boolean isSourceCopy() { return myRoleInParent == null; } public void fillNodeMap(INodeIdToNodeMap map) { assert !isSourceCopy(); for (SNode r : myReplacement) { for (SNode n : SNodeUtil.getDescendants(r)) { map.put(n.getNodeId(), n); } } } } }