/*
* Copyright 2003-2016 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.newTypesystem.context.typechecking;
import gnu.trove.THashMap;
import gnu.trove.THashSet;
import jetbrains.mps.classloading.ClassLoaderManager;
import jetbrains.mps.classloading.MPSClassesListener;
import jetbrains.mps.classloading.MPSClassesListenerAdapter;
import jetbrains.mps.errors.IErrorReporter;
import jetbrains.mps.lang.typesystem.runtime.ICheckingRule_Runtime;
import jetbrains.mps.lang.typesystem.runtime.IsApplicableStatus;
import jetbrains.mps.logging.Logger;
import jetbrains.mps.module.ReloadableModuleBase;
import jetbrains.mps.newTypesystem.context.component.ITypeErrorComponent;
import jetbrains.mps.newTypesystem.context.component.IncrementalTypecheckingComponent;
import jetbrains.mps.newTypesystem.context.component.NonTypeSystemComponent;
import jetbrains.mps.newTypesystem.context.component.TypeSystemComponent;
import jetbrains.mps.newTypesystem.state.State;
import jetbrains.mps.smodel.DynamicReference;
import jetbrains.mps.smodel.MPSModuleRepository;
import jetbrains.mps.smodel.SModelAdapter;
import jetbrains.mps.smodel.SModelInternal;
import jetbrains.mps.smodel.event.SModelChildEvent;
import jetbrains.mps.smodel.event.SModelEvent;
import jetbrains.mps.smodel.event.SModelEventVisitorAdapter;
import jetbrains.mps.smodel.event.SModelPropertyEvent;
import jetbrains.mps.smodel.event.SModelReferenceEvent;
import jetbrains.mps.typesystem.inference.TypeChecker;
import jetbrains.mps.typesystem.inference.TypeCheckingContext;
import jetbrains.mps.typesystem.inference.TypeRecalculatedListener;
import jetbrains.mps.util.Cancellable;
import jetbrains.mps.util.IterableUtil;
import jetbrains.mps.util.Pair;
import jetbrains.mps.util.WeakSet;
import org.apache.log4j.LogManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.mps.openapi.model.SModel;
import org.jetbrains.mps.openapi.model.SNode;
import org.jetbrains.mps.openapi.model.SNodeUtil;
import org.jetbrains.mps.openapi.model.SReference;
import org.jetbrains.mps.openapi.module.SRepositoryContentAdapter;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class IncrementalTypechecking extends BaseTypechecking<State, TypeSystemComponent> {
private List<SModelEvent> myEvents = new ArrayList<SModelEvent>();
private List<SModel> myReplacedModels = new ArrayList<SModel>();
private MPSClassesListener myClassesListener = new MPSClassesListenerAdapter() {
@Override
public void beforeClassesUnloaded(Set<? extends ReloadableModuleBase> unloadedModules) {
myNonTypeSystemComponent.clear();
}
};
private Map<SModel, Set<SNode>> mySModelNodes = new THashMap<SModel, Set<SNode>>();
private MyTypeRecalculatedListener myTypeRecalculatedListener = new MyTypeRecalculatedListener();
private MyModelListener myModelListener = new MyModelListener();
private MyModelListenerManager myModelListenerManager = new MyModelListenerManager();
private MySmodelListener mySModelListener = new MySmodelListener();
private NonTypeSystemComponent myNonTypeSystemComponent;
private static final Logger LOG = Logger.wrap(LogManager.getLogger(IncrementalTypechecking.class));
private NodeTypeAccess myNodeTypeAccess = new NodeTypeAccess();
private ITypeErrorComponent myTypeErrorComponent;
public IncrementalTypechecking(SNode node, State state) {
super(node, state);
myNonTypeSystemComponent = new NonTypeSystemComponent(TypeChecker.getInstance(), state, this);
myModelListenerManager.track(myRootNode);
ClassLoaderManager.getInstance().addClassesHandler(myClassesListener);
}
@Override
protected TypeSystemComponent createTypecheckingComponent() {
return new TypeSystemComponent(TypeChecker.getInstance(), getState(), this);
}
public void clear() {
clearNodesTypes();
myNonTypeSystemComponent.clear();
getTypecheckingComponent().clear();
}
public MyModelListenerManager getModelListenerManager() {
return myModelListenerManager;
}
public MyTypeRecalculatedListener getTypeRecalculatedListener() {
return myTypeRecalculatedListener;
}
private void clearNodesTypes() {
getTypecheckingComponent().clearNodeTypes();
myNonTypeSystemComponent.clearNodeTypes();
}
private void putError(SNode node, IErrorReporter reporter) {
getTypeErrorComponent().addError(node, reporter);
}
private ITypeErrorComponent getTypeErrorComponent() {
return myTypeErrorComponent != null ? myTypeErrorComponent : getTypecheckingComponent();
}
public void reportTypeError(SNode nodeWithError, IErrorReporter errorReporter) {
if (nodeWithError != null) {
putError(nodeWithError, errorReporter);
}
}
public void applyRuleToNode(SNode node, ICheckingRule_Runtime rule, IsApplicableStatus status, TypeCheckingContext typeCheckingContext) {
try {
rule.applyRule(node, typeCheckingContext, status);
} catch (Throwable t) {
LOG.error("an error occurred while applying rule to node " + node, t, node);
}
}
/**
* Returns true if the node's type is affected.
*/
public boolean runApplyRulesTo(SNode node, Runnable run) {
myNodeTypeAccess.pushNode(node);
try {
run.run();
} finally {
return myNodeTypeAccess.popNode();
}
}
@Override
public void dispose() {
ClassLoaderManager.getInstance().removeClassesHandler(myClassesListener);
if (myModelListenerManager != null) {
myModelListenerManager.dispose();
myModelListenerManager = null;
}
TypeChecker.getInstance().removeTypeRecalculatedListener(myTypeRecalculatedListener);
if (myNonTypeSystemComponent != null) {
myNonTypeSystemComponent = null;
}
mySModelListener = null;
super.dispose();
}
public void setCheckedNonTypesystem() {
myNonTypeSystemComponent.setChecked();
}
public void typeOfNodeCalled(SNode node) {
myNodeTypeAccess.nodeTypeAccessed(node);
}
public void addDependencyOnCurrent(SNode node, boolean typeAffected) {
addDependencyOnCurrent_(node, typeAffected);
}
public void addDependencyOnCurrent(SNode node) {
addDependencyOnCurrent_(node, true);
}
//"type affected" means that *type* of this node depends on current
// used to decide whether call "type will be recalculated" if current invalidated
private void addDependencyOnCurrent_(SNode node, boolean typeAffected) {
if (node == null) {
LOG.error("Typesystem dependency not tracked. ");
return;
}
Set<SNode> hashSet = new THashSet<SNode>(1);
hashSet.add(myNodeTypeAccess.peekNode());
getTypecheckingComponent().addDependentNodesTypeSystem(node, hashSet, typeAffected);
}
public void addDependencyForCurrent(SNode node) {
SNode current = myNodeTypeAccess.peekNode();
if (current == null) {
LOG.error("Typesystem dependency not tracked. ");
return;
}
Set<SNode> hashSet = new THashSet<SNode>(1);
hashSet.add(node);
getTypecheckingComponent().addDependentNodesTypeSystem(current, hashSet, true);
}
@Override
public boolean applyNonTypesystemRulesToRoot(TypeCheckingContext typeCheckingContext, Cancellable c) {
ITypeErrorComponent oldTypeErrorComponent = myTypeErrorComponent;
myTypeErrorComponent = myNonTypeSystemComponent;
try {
return myNonTypeSystemComponent.applyNonTypeSystemRulesToRoot(typeCheckingContext, getNode(), c);
} finally {
myTypeErrorComponent = oldTypeErrorComponent;
}
}
public SNode getType(SNode node) {
return getTypecheckingComponent().getType(node);
}
@Override
@NotNull
public List<IErrorReporter> getErrors(SNode node) {
List<IErrorReporter> result = new ArrayList<IErrorReporter>(super.getErrors(node));
Map<SNode, List<IErrorReporter>> nodesToErrorsMapNT = myNonTypeSystemComponent.getNodesToErrorsMap();
List<IErrorReporter> iErrorReporters = nodesToErrorsMapNT.get(node);
if (iErrorReporters != null) {
result.addAll(iErrorReporters);
}
return result;
}
//--------------------------------------------------
@Override
public Set<Pair<SNode, List<IErrorReporter>>> getNodesWithErrors(boolean typesystemErrors) {
if (typesystemErrors) {
return new THashSet<Pair<SNode, List<IErrorReporter>>>(super.getNodesWithErrors(typesystemErrors));
}
Set<Pair<SNode, List<IErrorReporter>>> result = new THashSet<Pair<SNode, List<IErrorReporter>>>();
Map<SNode, List<IErrorReporter>> nodesToErrorsMapNT = myNonTypeSystemComponent.getNodesToErrorsMap();
Set<SNode> keySet = new THashSet<SNode>(nodesToErrorsMapNT.keySet());
keySet.addAll(nodesToErrorsMapNT.keySet());
for (SNode key : keySet) {
List<IErrorReporter> reporters = nodesToErrorsMapNT.get(key);
if (reporters.isEmpty()) continue;
if (key.getModel() == null) {
LOG.warning("Type system reports error for node without containing root. Node: " + key);
for (IErrorReporter reporter : reporters) {
LOG.warning("This error was reported from: " + reporter.getRuleNode(), reporter.getRuleNode());
}
continue;
}
result.add(new Pair<SNode, List<IErrorReporter>>(key, reporters));
}
return result;
}
public void markNodeAsAffectedByRule(SNode node, String ruleModel, String ruleId) {
getTypecheckingComponent().markNodeAsAffectedByRule(node, ruleModel, ruleId);
}
public Set<Pair<String, String>> getRulesWhichAffectNodeType(SNode node) {
return getTypecheckingComponent().getRulesWhichAffectNodeType(node);
}
public boolean isCheckedNonTypesystem() {
return myNonTypeSystemComponent.isChecked();
}
@Override
public boolean isChecked(boolean considerNonTypeSystemRules) {
processPendingEvents();
boolean typesChecked = super.isChecked(considerNonTypeSystemRules);
if (considerNonTypeSystemRules) {
return typesChecked && myNonTypeSystemComponent.isChecked();
} else {
return typesChecked;
}
}
private void processPendingEvents() {
final MySModelEventVisitorAdapter visitor = new MySModelEventVisitorAdapter();
for (SModelEvent event : myEvents) {
event.accept(visitor);
}
for (SModel replacedModel : myReplacedModels) {
for (SNode node : mySModelNodes.get(replacedModel)) {
visitor.markInvalid(node);
}
}
myReplacedModels.clear();
myEvents.clear();
}
public void track(SNode node) {
myModelListenerManager.track(node);
}
public void updateGCedNodes() {
getModelListenerManager().updateGCedNodes();
}
private class MyModelListener extends SModelAdapter {
@Override
public void eventFired(SModelEvent event) {
myEvents.add(event);
}
}
private class MySmodelListener extends SRepositoryContentAdapter {
@Override
public void modelReplaced(SModel model) {
myReplacedModels.add(model);
}
}
private class MySModelEventVisitorAdapter extends SModelEventVisitorAdapter {
@Override
public void visitChildEvent(SModelChildEvent event) {
final SNode child = event.getChild();
final SNode parent = event.getParent();
if (jetbrains.mps.smodel.SNodeUtil.isSideTransformInfo(child) &&
(event.isRemoved() || jetbrains.mps.smodel.SNodeUtil.link_BaseConcept_smodelAttribute.equals(child.getContainmentLink()))) {
return;
}
markInvalid(child);
markInvalid(parent);
List<SNode> childWithDescendants = IterableUtil.copyToList(SNodeUtil.getDescendants(child, null, true));
if (event.isRemoved()) {
Iterator<SNode> it = childWithDescendants.iterator();
it.next(); // skip child, we've marked it as invalid already
while (it.hasNext()) {
SNode descendant = it.next();
//invalidate nodes which are removed
markDependentNodesForInvalidation(descendant, myNonTypeSystemComponent);
markDependentNodesForInvalidation(descendant, getTypecheckingComponent());
}
}
List<SReference> references = new ArrayList<SReference>();
for (SNode descendant : childWithDescendants) {
references.addAll(IterableUtil.asCollection(descendant.getReferences()));
}
markReferenceTargetsInvalid(references);
}
private void markReferenceTargetsInvalid(List<SReference> references) {
for (SReference reference : references) {
// MPS-18585 IncrementalTypecheking doesn't invalidate target nodes of dynamic refs if source node has been detached from model
if (reference instanceof DynamicReference) {
// the problem was in a more strict case:
// dynamic reference from a detached node (its getTargetNode() seems to be non-sensible)
// but I skip all DynamicReferences
continue;
}
SNode targetNode = jetbrains.mps.util.SNodeOperations.getTargetNodeSilently(reference);
if (targetNode != null) {
markDependentNodesForInvalidation(targetNode, myNonTypeSystemComponent);
}
}
}
@Override
public void visitReferenceEvent(SModelReferenceEvent event) {
SReference ref = event.getReference();
markInvalid(ref.getSourceNode());
// A heuristic: always invalidate the node's parent (MPS-21481)
if (ref.getSourceNode().getParent() != null) {
markInvalid(ref.getSourceNode().getParent());
}
if (!event.isAdded()) return;
// MPS-18585 IncrementalTypecheking doesn't invalidate target nodes of dynamic refs if source node has been detached from model
if (ref instanceof DynamicReference && ref.getSourceNode().getModel() == null) {
return;
}
SNode node = jetbrains.mps.util.SNodeOperations.getTargetNodeSilently(event.getReference());
if (node == null) return;
markDependentNodesForInvalidation(node, myNonTypeSystemComponent);
}
@Override
public void visitPropertyEvent(SModelPropertyEvent event) {
markDependentOnPropertyNodesForInvalidation(event.getNode(), event.getPropertyName());
}
private void markInvalid(SNode node) {
markDependentNodesForInvalidation(node, getTypecheckingComponent());
markDependentNodesForInvalidation(node, myNonTypeSystemComponent);
}
private void markDependentNodesForInvalidation(SNode eventNode, IncrementalTypecheckingComponent component) {
component.addNodeToInvalidate(eventNode);
}
private void markDependentOnPropertyNodesForInvalidation(SNode eventNode, String propertyName) {
myNonTypeSystemComponent.addPropertyToInvalidate(eventNode, propertyName);
getTypecheckingComponent().addNodeToInvalidate(eventNode);
}
}
private class MyTypeRecalculatedListener implements TypeRecalculatedListener {
MyTypeRecalculatedListener() {
}
@Override
public void typeWillBeRecalculatedForTerm(SNode term) {
myNonTypeSystemComponent.typeWillBeRecalculatedForTerm(term);
}
}
private class MyModelListenerManager {
private ReferenceQueue<SNode> myReferenceQueue = new ReferenceQueue<SNode>();
private Map<SModel, Integer> myNodesCount = new THashMap<SModel, Integer>();
private Map<WeakReference, SModel> myDescriptors = new THashMap<WeakReference, SModel>();
/**
* Warning: this method should be called only once for each node
* We do not check for duplicated nodes
*/
void track(SNode node) {
if (!SNodeUtil.isAccessible(node, MPSModuleRepository.getInstance())) return;
SModel sm = node.getModel();
if (!myNodesCount.containsKey(sm)) {
((SModelInternal) sm).addModelListener(myModelListener);
sm.addModelListener(mySModelListener);
myNodesCount.put(sm, 1);
mySModelNodes.put(sm, new WeakSet<SNode>());
} else {
Integer oldValue = myNodesCount.get(sm);
myNodesCount.put(sm, oldValue + 1);
}
WeakReference<SNode> ref = new WeakReference<SNode>(node, myReferenceQueue);
myDescriptors.put(ref, sm);
mySModelNodes.get(sm).add(node);
}
void updateGCedNodes() {
while (true) {
WeakReference<SNode> ref = (WeakReference<SNode>) myReferenceQueue.poll();
if (ref == null) return;
SModel sm = myDescriptors.get(ref);
Integer count = myNodesCount.get(sm);
if (count == 1) {
((SModelInternal) sm).removeModelListener(myModelListener);
sm.removeModelListener(mySModelListener);
myNodesCount.remove(sm);
mySModelNodes.remove(sm);
} else {
myNodesCount.put(sm, count - 1);
}
myDescriptors.remove(ref);
}
}
void dispose() {
for (SModel sm : Collections.unmodifiableCollection(myNodesCount.keySet())) {
((SModelInternal) sm).removeModelListener(myModelListener);
sm.removeModelListener(mySModelListener);
}
}
}
private static class NodeTypeAccess {
private LinkedList<Pair<SNode, Boolean>> myStack = new LinkedList<Pair<SNode, Boolean>>();
private void pushNode(SNode node) {
myStack.push(new Pair<SNode, Boolean>(node, false));
}
private boolean popNode() {
return myStack.pop().o2;
}
private void nodeTypeAccessed(SNode node) {
for (Pair<SNode, Boolean> p : myStack) {
if (p.o1 == node) {
p.o2 = true;
}
}
}
private SNode peekNode() {
if (myStack.isEmpty()) return null;
return myStack.peek().o1;
}
}
}