/* * Copyright 2003-2017 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.typesystem.inference.util; import jetbrains.mps.lang.pattern.ConceptMatchingPattern; import jetbrains.mps.lang.pattern.GeneratedMatchingPattern; import jetbrains.mps.lang.pattern.IMatchingPattern; import jetbrains.mps.smodel.SNodeUtil; import jetbrains.mps.util.Pair; import org.jetbrains.annotations.Nullable; import org.jetbrains.mps.openapi.language.SAbstractConcept; import org.jetbrains.mps.openapi.model.SModel; import org.jetbrains.mps.openapi.model.SNode; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; public class ConcurrentSubtypingCache implements SubtypingCache { private ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<CacheNodeHandler, MyBoolean>> myCache = new ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<CacheNodeHandler, MyBoolean>>(); private ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<CacheNodeHandler, MyBoolean>> myCacheWeak = new ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<CacheNodeHandler, MyBoolean>>(); private ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<String, SNode>> myCoerceToConceptsCache = new ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<String, SNode>>(); private ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<String, SNode>> myCoerceToConceptsCacheWeak = new ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<String, SNode>>(); private ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<Class, Pair<SNode, GeneratedMatchingPattern>>> myCoerceToPatternsCache = new ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<Class, Pair<SNode, GeneratedMatchingPattern>>>(); private ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<Class, Pair<SNode, GeneratedMatchingPattern>>> myCoerceToPatternsCacheWeak = new ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<Class, Pair<SNode, GeneratedMatchingPattern>>>(); private static final jetbrains.mps.smodel.SNode NULL = new jetbrains.mps.smodel.SNode(SNodeUtil.concept_BaseConcept); private SNode preprocessPutNode(SNode node) { return node == null ? NULL : node; } private SNode postprocessGetNode(SNode node) { return node == NULL ? null : node; } private MyBoolean preprocessPutBoolean(Boolean b) { if (b == null) return MyBoolean.NULL; return b ? MyBoolean.TRUE : MyBoolean.FALSE; } private Boolean postprocessGetBoolean(MyBoolean b) { if (b == null) return null; switch (b) { case FALSE: return false; case TRUE: return true; default: return null; } } @Override public void cacheIsSubtype(SNode subtype, SNode supertype, boolean answer, boolean isWeak) { ConcurrentMap<CacheNodeHandler, ConcurrentMap<CacheNodeHandler, MyBoolean>> cache = isWeak ? myCacheWeak : myCache; final CacheNodeHandler subtypeHandler = new CacheNodeHandler(subtype); ConcurrentMap<CacheNodeHandler, MyBoolean> supertypes = cache.get(subtypeHandler); if (supertypes == null) { supertypes = new ConcurrentHashMap<CacheNodeHandler, MyBoolean>(); if (cache.putIfAbsent(subtypeHandler, supertypes) != null) { supertypes = cache.get(subtypeHandler); } } if (supertypes != null) { supertypes.put(new CacheNodeHandler(supertype), preprocessPutBoolean(answer)); } } @Override public Boolean getIsSubtype(SNode subtype, SNode supertype, boolean isWeak) { CacheNodeHandler subtypeHandler = new CacheNodeHandler(subtype); // lookup in the corresponding cache Map<CacheNodeHandler, MyBoolean> supertypes = (isWeak ? myCacheWeak : myCache).get(subtypeHandler); if (supertypes != null) { MyBoolean result = supertypes.get(new CacheNodeHandler(supertype)); if (result != null) return postprocessGetBoolean(result); } // isStrong => isWeak; !isWeak => !isStrong supertypes = (isWeak ? myCache : myCacheWeak).get(subtypeHandler); if (supertypes != null) { MyBoolean result = supertypes.get(new CacheNodeHandler(supertype)); if (isWeak) { if (result == MyBoolean.TRUE /* isStrong */) return Boolean.TRUE; // isWeak } else { if (result == MyBoolean.FALSE /* !isWeak */) return Boolean.FALSE; // !isStrong } } return null; } private Pair<Boolean, SNode> getCoerced(SNode subtype, SAbstractConcept concept, boolean isWeak) { String conceptFQName = concept.getQualifiedName(); final CacheNodeHandler subtypeHandler = new CacheNodeHandler(subtype); // lookup in the corresponding cache ConcurrentMap<String, SNode> map = (isWeak ? myCoerceToConceptsCacheWeak : myCoerceToConceptsCache).get(subtypeHandler); if (map != null) { SNode value = map.get(conceptFQName); if (value != null) { SNode result = postprocessGetNode(value); if (result != null && !isAccessible(result, subtype)) { map.remove(conceptFQName); } else { return new Pair<Boolean, SNode>(true, result); } } } // isStrong => isWeak; !isWeak => !isStrong map = (isWeak ? myCoerceToConceptsCache : myCoerceToConceptsCacheWeak).get(subtypeHandler); if (map != null) { SNode value = map.get(conceptFQName); if (value != null) { SNode result = postprocessGetNode(value); if (isWeak) { if (result != null /* isStrong */) return new Pair<Boolean, SNode>(true, result); // isWeak } else { if (result == null /* !isWeak */) return new Pair<Boolean, SNode>(true, null); // !isStrong } } } return null; } private Pair<Boolean, SNode> getCoerced(SNode subtype, Class c, GeneratedMatchingPattern pattern, boolean isWeak) { Map<CacheNodeHandler, ConcurrentMap<Class, Pair<SNode, GeneratedMatchingPattern>>> cache = isWeak ? myCoerceToPatternsCacheWeak : myCoerceToPatternsCache; Map<Class, Pair<SNode, GeneratedMatchingPattern>> map = cache.get(new CacheNodeHandler(subtype)); if (map != null && map.containsKey(c)) { Pair<SNode, GeneratedMatchingPattern> patternPair = map.get(c); SNode resultNode = patternPair.o1; if (resultNode != null && !isAccessible(resultNode, subtype)) { map.remove(c); return null; } else { // XXX what's the difference between null return value, and Pair(true, null)? pattern.fillFieldValuesFrom(patternPair.o2); return new Pair<Boolean, SNode>(true, resultNode); } } return null; } /** * The idea of cached nodes in a global instance is flawed (no respect to context repository), but it's impossible to fix without enormous effort. * Here, to avoid use of global repository, we check if a result node would be accessible to caller, based on * a repository of the node caller had supplied us. * Alternative approach would be to keep result node's repository in the cache and check against that repository, although it bring troubles for short-lived * repositories (e.g. one with transients in generation, once we have one). * @param resultNode not null * @param contextNode not null * @return Tells if those with access to context node may access resultNode, too. */ private boolean isAccessible(SNode resultNode, SNode contextNode) { SModel m = contextNode.getModel(); if (m == null || m.getRepository() == null) { // node we check subtype for is hanging in the air, no idea whether it could use resultNode // it's unlikely m == null, but it doesn't hurt to check. return false; } return org.jetbrains.mps.openapi.model.SNodeUtil.isAccessible(resultNode, m.getRepository()); } private void addCacheEntry(SNode subtype, SAbstractConcept concept, SNode result, boolean isWeak) { ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<String, SNode>> cache = isWeak ? myCoerceToConceptsCacheWeak : myCoerceToConceptsCache; CacheNodeHandler subtypeHandler = new CacheNodeHandler(subtype); ConcurrentMap<String, SNode> map = cache.get(subtypeHandler); if (map == null) { map = new ConcurrentHashMap<String, SNode>(); if (cache.putIfAbsent(subtypeHandler, map) != null) { map = cache.get(subtypeHandler); } } if (map != null) { map.put(concept.getQualifiedName(), preprocessPutNode(result)); } } private void addCacheEntry(SNode subtype, Class c, SNode result, GeneratedMatchingPattern pattern, boolean isWeak) { ConcurrentHashMap<CacheNodeHandler, ConcurrentMap<Class, Pair<SNode, GeneratedMatchingPattern>>> cache = isWeak ? myCoerceToPatternsCacheWeak : myCoerceToPatternsCache; CacheNodeHandler subtypeHandler = new CacheNodeHandler(subtype); ConcurrentMap<Class, Pair<SNode, GeneratedMatchingPattern>> map = cache.get(subtypeHandler); if (map == null) { map = new ConcurrentHashMap<Class, Pair<SNode, GeneratedMatchingPattern>>(); if (cache.putIfAbsent(subtypeHandler, map) != null) { map = cache.get(subtypeHandler); } } if (map != null) { map.put(c, new Pair<SNode, GeneratedMatchingPattern>(result, pattern)); } } @Override public void cacheCoerce(SNode subtype, IMatchingPattern pattern, SNode result, boolean isWeak) { if (pattern instanceof ConceptMatchingPattern) { addCacheEntry(subtype, pattern.getConcept(), result, isWeak); return; } if (pattern instanceof GeneratedMatchingPattern) { if (!((GeneratedMatchingPattern) pattern).hasAntiquotations()) { addCacheEntry(subtype, pattern.getClass(), result, (GeneratedMatchingPattern) pattern, isWeak); } } } @Override @Nullable public Pair<Boolean, SNode> getCoerced(SNode subtype, IMatchingPattern pattern, boolean isWeak) { if (pattern instanceof ConceptMatchingPattern) { return getCoerced(subtype, pattern.getConcept(), isWeak); } if (pattern instanceof GeneratedMatchingPattern) { if (!((GeneratedMatchingPattern) pattern).hasAntiquotations()) { return getCoerced(subtype, pattern.getClass(), (GeneratedMatchingPattern) pattern, isWeak); } } return null; } private static enum MyBoolean { NULL, FALSE, TRUE } }