/* BSD 2-Clause License - see OPAL/LICENSE for details. */ package org.opalj package fpcf import java.util.concurrent.ConcurrentHashMap import java.util.{Arrays => JArrays} import java.util.concurrent.RejectedExecutionException import scala.util.control.ControlThrowable import scala.collection.mutable import org.opalj.log.GlobalLogContext import org.opalj.log.LogContext import org.opalj.log.OPALLogger.info import org.opalj.log.OPALLogger.{debug => trace} import org.opalj.log.OPALLogger.error import org.opalj.collection.IntIterator import org.opalj.fpcf.PropertyKind.SupportedPropertyKinds import org.opalj.fpcf.PropertyKey.fallbackPropertyBasedOnPKId /** * A property store manages the execution of computations of properties related to concrete * entities as well as artificial entities (for example, methods, fields and classes of a program, * but, for another example, also the call graph or the project as such). These computations may * require and provide information about other entities of the store and the property store * implements the logic to handle the computations related to the dependencies between the entities. * Furthermore, the property store may parallelize the computation of the properties as far as * possible without requiring users to take care of it; * users are also generally not required to think about the concurrency when implementing an * analysis as long as the properties only use immutable data-structures and the analyses * only use immutable data structures when interacting the property store. * The most basic concepts are also described in the SOAP paper: * "Lattice Based Modularization of Static Analyses" * (https://conf.researchr.org/event/issta-2018/soap-2018-papers-lattice-based-modularization-of-static-analyses) * * ==Usage== * The correct strategy, when using the PropertyStore, is to always continue computing the property * of an entity and to collect the dependencies on those elements that are (still) relevant. * I.e., if some information is not or just not completely available, the analysis should * still continue using the provided information and (internally) record the dependency, by storing * the returned property extension. * Later on, when the analysis has computed its (interim) result, it reports the same and informs * the framework about its dependencies. * Based on the later the framework will call back the analysis when a dependency is updated. * In general, an analysis should always try to minimize the number * of dependencies to the minimum set to enable the property store to suspend computations that * are no longer required. * * ===Core Requirements on Property Computation Functions (Modular Static Analyses)=== * The following requirements ensure correctness and determinism of the result. * - '''At Most One Lazy Function per Property Kind''' A specific kind of property is * always computed by only one registered lazy `PropertyComputation` function. * No other analysis is (conceptually) allowed to derive a value for an E/PK pairing * for which a lazy function is registered. It is also not allowed to schedule a computation * eagerly if a lazy computation is also registered. * * - '''Thread-Safe PropertyComputation functions''' If a single instance of a property computation * function (which is the standard case) is scheduled for computing the properties of multiple * entities, that function has to be thread safe. I.e., the function may * be executed concurrently for different entities. The [[OnUpdateContinuation]] functions * are, however, executed sequentially w.r.t. one E/PK pair. This model generally does not * require that users have to think about concurrent issues as long as the initial function * is actually a pure function, which is usually a non-issue. * * - '''Non-Overlapping Results''' [[PropertyComputation]] functions that are invoked on different * entities have to compute result sets that are disjoint unless a [[PartialResult]] is used. * For example, an analysis that performs a computation on class files and * that derives properties of a specific kind related to a class file's methods must ensure * that two concurrent calls of the same analysis - running concurrently on two different * class files - do not derive information about the same method. If results for a specific * entity are collaboratively computed, then a [[PartialResult]] has to be used. * * - '''If some partial result potentially contributes to the property of an entity, * the first partial result has to set the property to the default (typically "most precise") * value.''' * * - '''Monoton''' a function which computes a property has to be monotonic. * * ===Cyclic Dependencies=== * In general, it may happen that some analyses are mutually dependent and therefore no * final value is directly computed. In this case the current extension (the most precise result) * of the properties are committed as the final values when the phase end. If the analyses only * computed a lower bound that one will be used. * * ==Thread Safety== * The sequential property store is not thread-safe; the parallelized implementation enables * limited concurrent access: * - a client has to use the SAME thread (the driver thread) to call * (0) [[set]] and [[preInitialize]] to initialize the property store, * (1) [[org.opalj.fpcf.PropertyStore!.setupPhase(configuration:org\.opalj\.fpcf\.PropertyKindsConfiguration)*]], * (2) [[registerLazyPropertyComputation]] or [[registerTriggeredComputation]], * (3) [[scheduleEagerComputationForEntity]] / [[scheduleEagerComputationsForEntities]], * (4) [[force]] and * (5) (finally) [[PropertyStore#waitOnPhaseCompletion]]. * go back to (1). * Hence, the previously mentioned methods MUST NOT be called by * PropertyComputation/OnUpdateComputation functions. The methods to query the store (`apply`) * are thread-safe and can be called at any time. * * ==Common Abbreviations== * - e = Entity * - p = Property * - pk = Property Key * - pc = Property Computation * - lpc = Lazy Property Computation * - c = Continuation (The part of the analysis that factors in all properties of dependees) * - EPK = Entity and a PropertyKey * - EPS = Entity, Property and the State (final or intermediate) * - EP = Entity and some (final or intermediate) Property * - EOptionP = Entity and either a PropertyKey or (if available) a Property * - ps = Property Store * * ==Exceptions== * In general, exceptions are only thrown if debugging is turned on due to the costs of checking * for the respective violations. That is, if debugging is turned off, many potential errors leading * to "incomprehensible" results will not be reported. Hence, after debugging an analysis turn * debugging (and assertions!) off to get the best performance. * * We will throw `IllegalArgumentException`'s iff a parameter is in itself invalid. E.g., the lower * bound is ``above`` the upper bound. In all other cases `IllegalStateException`s are thrown. * All exceptions are either thrown immediately or eventually, when * [[PropertyStore#waitOnPhaseCompletion]] is called. In the latter case, the exceptions are * accumulated in the first thrown exception using suppressed exceptions. * * @author Michael Eichberg */ abstract class PropertyStore { implicit val logContext: LogContext // // // FUNCTIONALITY TO ASSOCIATE SOME INFORMATION WITH THE STORE THAT // (TYPICALLY) HAS THE SAME LIFETIME AS THE STORE // // private[this] val externalInformation = new ConcurrentHashMap[AnyRef, AnyRef]() /** * Attaches or returns some information associated with the property store using a key object. * * This method is thread-safe. However, the client which adds information to the store * has to ensure that the overall process of adding/querying/removing is well defined and * the ordered is ensured. */ final def getOrCreateInformation[T <: AnyRef](key: AnyRef, f: => T): T = { externalInformation.computeIfAbsent(key, _ => f).asInstanceOf[T] } /** * Returns the information stored in the store, if any. * * This method is thread-safe. However, the client which adds information to the store * has to ensure that the overall process of adding/querying/removing is well defined and * the ordered is ensured. */ final def getInformation[T <: AnyRef](key: AnyRef): Option[T] = { Option(externalInformation.get(key).asInstanceOf[T]) } /** * Returns the information stored in the store and removes the key, if any. * * This method is thread-safe. However, the client which adds information to the store * has to ensure that the overall process of adding/querying/removing is well defined and * the ordered is ensured. */ final def getAndClearInformation[T <: AnyRef](key: AnyRef): Option[T] = { Option(externalInformation.remove(key).asInstanceOf[T]) } // // // CONTEXT RELATED FUNCTIONALITY // (Required by analyses that use the property store to query the context.) // // /** Immutable map which stores the context objects given at initialization time. */ val ctx: Map[Class[_], AnyRef] /** * Looks up the context object of the given type. This is a comparatively expensive operation; * the result should be cached. */ final def context[T](key: Class[T]): T = { ctx.getOrElse(key, { throw ContextNotAvailableException(key, ctx) }).asInstanceOf[T] } // // // INTERRUPTION RELATED FUNCTIONALITY // (Required by the property store to determine if it should abort executing tasks.) // // /** * If set to `true` no new computations will be scheduled and running computations will * be terminated. Afterwards, the store can be queried, but no new computations can * be started. */ @volatile var doTerminate: Boolean = false /** * Should be called when a PropertyStore is no longer going to be used to schedule * computations. * * Properties can still be queried after shutdown. */ def shutdown(): Unit // // // DEBUGGING AND COMPREHENSION RELATED FUNCTIONALITY // // /** * If "debug" is `true` and we have an update related to an ordered property, * we will then check if the update is correct! */ final val debug: Boolean = PropertyStore.Debug // TODO Rename to "Debug" final val traceFallbacks: Boolean = PropertyStore.TraceFallbacks // TODO Rename to "TraceFallbacks" final val traceSuppressedNotifications: Boolean = PropertyStore.TraceSuppressedNotifications // TODO Rename to "TraceSuppressedNotifications" /** * Returns a consistent snapshot of the stored properties. * * @note Some computations may still be running. * * @param printProperties If `true` prints the properties of all entities. */ def toString(printProperties: Boolean): String /** * Returns a short string representation of the property store showing core figures. */ override def toString: String = toString(false) /** * Simple counter of the number of tasks that were executed to perform an initial * computation of a property for some entity. */ def scheduledTasksCount: Int /** * The number of ([[OnUpdateContinuation]]s) that were executed in response to an * updated property. */ def scheduledOnUpdateComputationsCount: Int /** The number of times the property store reached quiescence. */ def quiescenceCount: Int /** * The number of times a fallback property was computed for an entity though an (eager) * analysis was actually scheduled. */ def fallbacksUsedForComputedPropertiesCount: Int private[fpcf] def incrementFallbacksUsedForComputedPropertiesCounter(): Unit /** * Reports core statistics; this method is only guaranteed to report ''final'' results * if it is called while the store is quiescent. */ def statistics: mutable.LinkedHashMap[String, Int] = { val s = if (debug) mutable.LinkedHashMap( "scheduled tasks" -> scheduledTasksCount, "scheduled on update computations" -> scheduledOnUpdateComputationsCount, "computations of fallback properties for computed properties" -> fallbacksUsedForComputedPropertiesCount ) else mutable.LinkedHashMap.empty[String, Int] // Always available stats: s.put("quiescence", quiescenceCount) s } // // // CORE FUNCTIONALITY // // def MaxEvaluationDepth: Int /** * If a property is queried for which we have no value, then this information is used * to determine which kind of fallback is required. */ protected[this] final val propertyKindsComputedInEarlierPhase: Array[Boolean] = { new Array(SupportedPropertyKinds) } final def alreadyComputedPropertyKindIds: IntIterator = { IntIterator.upUntil(0, SupportedPropertyKinds).filter(propertyKindsComputedInEarlierPhase) } protected[this] final val propertyKindsComputedInThisPhase: Array[Boolean] = { new Array(SupportedPropertyKinds) } /** * Used to identify situations where a property is queried, which is only going to be computed * in the future - in this case, the specification of an analysis is broken! */ protected[this] final val propertyKindsComputedInLaterPhase: Array[Boolean] = { new Array(SupportedPropertyKinds) } protected[this] final val suppressInterimUpdates: Array[Array[Boolean]] = { Array.fill(SupportedPropertyKinds) { new Array[Boolean](SupportedPropertyKinds) } } /** * `true` if entities with a specific property kind (EP) may have dependers with suppressed * notifications. (I.e., suppressInteriumUpdates("depender")("EP") is `true`.) */ protected[this] final val hasSuppressedDependers: Array[Boolean] = { Array.fill(SupportedPropertyKinds) { false } } /** * The order in which the property kinds will be finalized; the last phase is considered * the clean-up phase and will contain all remaining properties that were not explicitly * finalized previously. */ protected[this] final var subPhaseFinalizationOrder: Array[List[PropertyKind]] = Array.empty /** * The set of computations that will only be scheduled if the result is required. */ protected[this] final val lazyComputations: Array[SomeProperPropertyComputation] = { new Array(PropertyKind.SupportedPropertyKinds) } /** * The set of transformers that will only be executed when required. */ protected[this] final val transformersByTargetPK: Array[( /*source*/ PropertyKey[Property], (Entity, Property) => FinalEP[Entity, Property])] = { new Array(PropertyKind.SupportedPropertyKinds) } protected[this] final val transformersBySourcePK: Array[( /*target*/ PropertyKey[Property], (Entity, Property) => FinalEP[Entity, Property])] = { new Array(PropertyKind.SupportedPropertyKinds) } protected[this] def computeFallback[E <: Entity, P <: Property]( e: E, pkId: Int ): FinalEP[E, P] = { val reason = { if (propertyKindsComputedInEarlierPhase(pkId) || propertyKindsComputedInThisPhase(pkId)) { if (debug) incrementFallbacksUsedForComputedPropertiesCounter() PropertyIsNotDerivedByPreviouslyExecutedAnalysis } else { PropertyIsNotComputedByAnyAnalysis } } val p = fallbackPropertyBasedOnPKId(this, reason, e, pkId) if (traceFallbacks) { trace("analysis progress", s"used fallback $p for $e") } FinalEP(e, p.asInstanceOf[P]) } /** * Returns `true` if the given entity is known to the property store. Here, `isKnown` can mean * - that we actually have a property, or * - a computation is scheduled/running to compute some property, or * - an analysis has a dependency on some (not yet finally computed) property, or * - that the store just eagerly created the data structures necessary to associate * properties with the entity because the entity was queried. */ def isKnown(e: Entity): Boolean /** * Tests if we have some (lb, ub or final) property for the entity with the respective kind. * If `hasProperty` returns `true` a subsequent `apply` will return an `EPS` (not an `EPK`). */ final def hasProperty(epk: SomeEPK): Boolean = hasProperty(epk.e, epk.pk) /** See `hasProperty(SomeEPK)` for details. **/ def hasProperty(e: Entity, pk: PropertyKind): Boolean /** * Returns an iterator of the different properties associated with the given entity. * * This method is the preferred way to get a snapshot of all properties of an entity and should * be used if you know that all properties are already computed. * * @note Only to be called when the store is quiescent. * @note Does not trigger lazy property computations. * * @param e An entity stored in the property store. */ def properties[E <: Entity](e: E): Iterator[EPS[E, Property]] /** * Returns all entities which have a property of the respective kind. The result is * undefined if this method is called while the property store still performs * (concurrent) computations. * * @note Only to be called when the store is quiescent. * @note Does not trigger lazy property computations. */ def entities[P <: Property](pk: PropertyKey[P]): Iterator[EPS[Entity, P]] /** * Returns all entities that currently have the given property bounds based on an "==" (equals) * comparison. (In case of final properties the bounds are equal.) * If some analysis only computes an upper or a lower bound and no final results exists, * that entity will be ignored. * * @note Only to be called when the store is quiescent. * @note Does not trigger lazy property computations. */ def entities[P <: Property](lb: P, ub: P): Iterator[Entity] /** * @note Only to be called when the store is quiescent. * @note Does not trigger lazy property computations. */ def entitiesWithLB[P <: Property](lb: P): Iterator[Entity] /** * * @note Only to be called when the store is quiescent. * @note Does not trigger lazy property computations. */ def entitiesWithUB[P <: Property](ub: P): Iterator[Entity] /** * The set of all entities which have an entity property state that passes * the given filter. * * @note Only to be called when the store is quiescent. * @note Does not trigger lazy property computations. */ def entities(propertyFilter: SomeEPS => Boolean): Iterator[Entity] /** * Returns all final entities with the given property. * * @note Only to be called when the store is quiescent. * @note Does not trigger lazy property computations. */ def finalEntities[P <: Property](p: P): Iterator[Entity] = { entities((otherEPS: SomeEPS) => otherEPS.isFinal && otherEPS.asFinal.p == p) } /** @see `get(epk:EPK)` for details. */ def get[E <: Entity, P <: Property](e: E, pk: PropertyKey[P]): Option[EOptionP[E, P]] /** * Returns the property of the respective property kind `pk` currently associated * with the given element `e`. Does not trigger any computations. */ def get[E <: Entity, P <: Property](epk: EPK[E, P]): Option[EOptionP[E, P]] /** * Associates the given property `p`, which has property kind `pk`, with the given entity * `e` iff `e` has no property of the respective kind. The set property is always final. * * '''Calling this method is only supported before any analysis is scheduled!''' * * One use case is an analysis that does use the property store while executing the analysis, * but which wants to store the results in the store. Such an analysis '''must * be executed before any other analysis is scheduled'''. * A second use case are (eager or lazy) analyses, which want to store some pre-configured * information in the property store; e.g., properties of natives methods which were derived * beforehand. * * @note This method must not be used '''if there might be a computation (in the future) that * computes the property kind `pk` for the given `e`'''. */ final def set(e: Entity, p: Property): Unit = { if (!isIdle) { throw new IllegalStateException("analyses are already running") } if (propertyKindsComputedInEarlierPhase(p.key.id)) { throw new IllegalStateException(s"property kind (of $p) was computed in previous phase") } doSet(e, p) } protected[this] def doSet(e: Entity, p: Property): Unit /** * Associates the given entity with the newly computed intermediate property P. * * '''Calling this method is only supported before any analysis is scheduled!''' * * @param pc A function which is given the current property of kind pk associated with e and * which has to compute the new '''intermediate''' property `p`. */ final def preInitialize[E <: Entity, P <: Property]( e: E, pk: PropertyKey[P] )( pc: EOptionP[E, P] => InterimEP[E, P] ): Unit = { if (!isIdle) { throw new IllegalStateException("analyses are already running") } if (propertyKindsComputedInEarlierPhase(pk.id)) { throw new IllegalStateException(s"property kind ($pk) was computed in previous phase") } doPreInitialize(e, pk)(pc) } protected[this] def doPreInitialize[E <: Entity, P <: Property]( e: E, pk: PropertyKey[P] )( pc: EOptionP[E, P] => InterimEP[E, P] ): Unit final def setupPhase(configuration: PropertyKindsConfiguration): Unit = { setupPhase( configuration.propertyKindsComputedInThisPhase, configuration.propertyKindsComputedInLaterPhase, configuration.suppressInterimUpdates, configuration.collaborativelyComputedPropertyKindsFinalizationOrder ) } protected[this] var subPhaseId: Int = 0 protected[this] var hasSuppressedNotifications: Boolean = false /** * Needs to be called before an analysis is scheduled to inform the property store which * properties will be computed now and which are computed in a later phase. The * information is used to decide when we use a fallback and which kind of fallback. * * @note `setupPhase` even needs to be called if just fallback values should be computed; in * this case `propertyKindsComputedInThisPhase` and `propertyKindsComputedInLaterPhase` * have to be empty, but `finalizationOrder` have to contain the respective property * kind. * * @param propertyKindsComputedInThisPhase The kinds of properties for which we will schedule * computations. * * @param propertyKindsComputedInLaterPhase The set of property kinds which will be computed * in a later phase. * @param suppressInterimUpdates Specifies which interim updates should not be passed to which * kind of dependers. * A depender will only be informed about the final update. The key of the map * identifies the target of a notification about an update (the depender) and the value * specifies which dependee updates should be ignored unless it is a final update. * This is an optimization related to lazy computations, but also enables the * implementation of transformers and the scheduling of analyses which compute different * kinds of bounds unless the analyses have cyclic dependencies. */ final def setupPhase( propertyKindsComputedInThisPhase: Set[PropertyKind], propertyKindsComputedInLaterPhase: Set[PropertyKind] = Set.empty, suppressInterimUpdates: Map[PropertyKind, Set[PropertyKind]] = Map.empty, finalizationOrder: List[List[PropertyKind]] = List.empty ): Unit = handleExceptions { if (!isIdle) { throw new IllegalStateException("computations are already running"); } require( suppressInterimUpdates.forall { e => val (dependerPK, dependeePKs) = e !dependeePKs.contains(dependerPK) }, "illegal self dependency" ) // Step 1 // Copy all property kinds that were computed in the previous phase that are no // longer computed to the "propertyKindsComputedInEarlierPhase" array. // Afterwards, initialize the "propertyKindsComputedInThisPhase" with the given // information. // Note that "lazy" property computations may be executed accross several phases, // however, all "intermediate" values found at the end of a phase can still be executed. this.propertyKindsComputedInThisPhase.iterator.zipWithIndex foreach { previousPhaseComputedPK => val (isComputed, pkId) = previousPhaseComputedPK if (isComputed && !propertyKindsComputedInThisPhase.exists(_.id == pkId)) { propertyKindsComputedInEarlierPhase(pkId) = true } } JArrays.fill(this.propertyKindsComputedInThisPhase, false) propertyKindsComputedInThisPhase foreach { pk => this.propertyKindsComputedInThisPhase(pk.id) = true } // Step 2 // Set the "propertyKindsComputedInLaterPhase" array to the specified values. JArrays.fill(this.propertyKindsComputedInLaterPhase, false) propertyKindsComputedInLaterPhase foreach { pk => this.propertyKindsComputedInLaterPhase(pk.id) = true } // Step 3 // Collect the information about which interim results should be suppressed. suppressInterimUpdates foreach { dependerDependees => val (depender, dependees) = dependerDependees require(dependees.nonEmpty) dependees foreach { dependee => this.suppressInterimUpdates(depender.id)(dependee.id) = true hasSuppressedDependers(dependee.id) = true } } // Step 4 // Save the information about the finalization order (of properties which are // collaboratively computed). val cleanUpSubPhase = (propertyKindsComputedInThisPhase -- finalizationOrder.flatten.toSet) + AnalysisKey this.subPhaseFinalizationOrder = if (cleanUpSubPhase.isEmpty) { finalizationOrder.toArray } else { (finalizationOrder :+ cleanUpSubPhase.toList).toArray } subPhaseId = 0 hasSuppressedNotifications = suppressInterimUpdates.nonEmpty // Step 5 // Call `newPhaseInitialized` to enable subclasses to perform custom initialization steps // when a phase was setup. newPhaseInitialized( propertyKindsComputedInThisPhase, propertyKindsComputedInLaterPhase, suppressInterimUpdates, finalizationOrder ) } /** * Called when a new phase was initialized. Intended to be overridden by subclasses if * special handling is required. */ protected[this] def newPhaseInitialized( propertyKindsComputedInThisPhase: Set[PropertyKind], propertyKindsComputedInLaterPhase: Set[PropertyKind], suppressInterimUpdates: Map[PropertyKind, Set[PropertyKind]], finalizationOrder: List[List[PropertyKind]] ): Unit = { /*nothing to do*/ } /** * Returns `true` if the store does not perform any computations at the time of this method * call. * * This method is only intended to support bug detection. */ def isIdle: Boolean /** * Returns a snapshot of the properties with the given kind associated with the given entities. * * @note Querying the properties of the given entities will trigger lazy computations. * @note The returned collection can be used to create an [[InterimResult]]. * @see `apply(epk:EPK)` for details. */ final def apply[E <: Entity, P <: Property]( es: Iterable[E], pk: PropertyKey[P] ): Iterable[EOptionP[E, P]] = { es.map(e => apply(EPK(e, pk))) } /** * Returns a snapshot of the properties with the given kind associated with the given entities. * * @note Querying the properties of the given entities will trigger lazy computations. * @note The returned collection can be used to create an [[InterimResult]]. * @see `apply(epk:EPK)` for details. */ final def apply[E <: Entity, P <: Property]( es: Iterable[E], pmi: PropertyMetaInformation { type Self <: P } ): Iterable[EOptionP[E, P]] = { apply(es, pmi.key) } /** @see `apply(epk:EPK)` for details. */ final def apply[E <: Entity, P <: Property](e: E, pk: PropertyKey[P]): EOptionP[E, P] = { apply(EPK(e, pk), e, pk, pk.id) } /** * Returns the property of the respective property kind `pk` currently associated * with the given element `e`. * * This is the most basic method to get some property and it is the preferred way * if (a) you know that the property is already available – e.g., because some * property computation function was strictly run before the current one – or * if (b) the property is computed using a lazy property computation - or * if (c) it may be possible to compute a final answer even if the property * of the entity is not yet available. * * @note In general, the returned value may change over time but only such that it * is strictly more precise. * @note Querying a property may trigger the (lazy) computation of the property. * @note [[setupPhase]] has to be called before calling apply! * @note After all computations has finished one of the "pure" query methods (e.g., * `entities` or `get` should be used.) * * @throws IllegalStateException If setup phase was not called or * a previous computation result contained an epk which was not queried. * (Both state are ALWAYS illegal, but are only explicitly checked for if debug * is turned on!) * @param epk An entity/property key pair. * @return `EPK(e,pk)` if information about the respective property is not (yet) available. * `Final|InterimP(e,Property)` otherwise. */ def apply[E <: Entity, P <: Property](epk: EPK[E, P]): EOptionP[E, P] = { val e = epk.e val pk = epk.pk val pkId = pk.id apply(epk, e, pk, pkId) } private[this] def apply[E <: Entity, P <: Property]( epk: EPK[E, P], e: E, pk: PropertyKey[P], pkId: Int ): EOptionP[E, P] = { if (debug && propertyKindsComputedInLaterPhase(pkId)) { throw new IllegalArgumentException( s"querying of property kind ($pk) computed in a later phase" ) } doApply(epk, e, pkId) } protected[this] def doApply[E <: Entity, P <: Property]( epk: EPK[E, P], e: E, pkId: Int ): EOptionP[E, P] /** * Enforce the evaluation of the specified property kind for the given entity, even * if the property is computed lazily and no "eager computation" requires the results * (anymore). * Using `force` is in particular necessary in cases where a specific analysis should * be scheduled lazily because the computed information is not necessary for all entities, * but strictly required for some elements. * E.g., if you want to compute a property for some piece of code, but not for those * elements of the used library that are strictly necessary. * For example, if we want to compute the purity of the methods of a specific application, * we may have to compute the property for some entities of the libraries, but we don't * want to compute them for all. * * @note Triggers lazy evaluations. */ def force[E <: Entity, P <: Property](e: E, pk: PropertyKey[P]): Unit /** * Registers a function that lazily computes a property for an element * of the store if the property of the respective kind is requested. * Hence, a first request of such a property will always first return no result. * * The computation is triggered by a(n in)direct call of this store's `apply` method. * * This store ensures that the property computation function `pc` is never invoked more * than once for the same element at the same time. If `pc` is invoked again for a specific * element then only because a dependee has changed! * * In general, the result can't be an `IncrementalResult`, a `PartialResult` or a `NoResult`. * * ''A lazy computation must never return a [[NoResult]]; if the entity cannot be processed an * exception has to be thrown or the bottom value – if defined – has to be returned.'' * * '''Calling `registerLazyPropertyComputation` is only supported as long as the store is not * queried and no computations are already running. * In general, this requires that lazy property computations are scheduled before any eager * analysis that potentially reads the value.''' */ final def registerLazyPropertyComputation[E <: Entity, P <: Property]( pk: PropertyKey[P], pc: ProperPropertyComputation[E] // TODO add definition of PropertyComputationResult that is parameterized over the kind of Property to specify that we want a PropertyComputationResult with respect to PropertyKey (pk) ): Unit = { if (!isIdle) { throw new IllegalStateException( "lazy computations can only be registered while the property store is idle" ) } lazyComputations(pk.id) = pc } /** * Registers a total function that takes a given final property and computes a new final * property of a different kind; the function must not query the property store. Furthermore, * `setupPhase` must specify that notifications about interim updates have to be suppressed. * A transformer is conceptually a special kind of lazy analysis. */ final def registerTransformer[SourceP <: Property, TargetP <: Property, E <: Entity]( sourcePK: PropertyKey[SourceP], targetPK: PropertyKey[TargetP] )( pc: (E, SourceP) => FinalEP[E, TargetP] ): Unit = { if (!isIdle) { throw new IllegalStateException( "transformers can only be registered while the property store is idle" ) } transformersByTargetPK(targetPK.id) = (sourcePK, pc.asInstanceOf[(Entity, Property) => FinalEP[Entity, Property]]) transformersBySourcePK(sourcePK.id) = (targetPK, pc.asInstanceOf[(Entity, Property) => FinalEP[Entity, Property]]) } /** * Registers a property computation that is eagerly triggered when a property of the given kind * is derived for some entity for the first time. Note, that the property computation * function – as usual – has to be thread safe (only on-update continuation functions are * guaranteed to be executed sequentially per E/PK pair). The primary use case is to * kick-start the computation of some e/pk as soon as an entity "becomes relevant". * * In general, it also possible to have a standard analysis that just queries the properties * of the respective entities and which maintains the list of dependees. However, if the * list of dependees becomes larger and (at least initially) encompasses a significant fraction * or even all entities of a specific kind, the overhead that is generated in the framework * becomes very huge. In this case, it is way more efficient to register a triggered * computation. * * For example, if you want to do some processing (kick-start further computations) related * to methods that are reached, it is more efficient to register a property computation * that is triggered when a method's `Caller` property is set. Please note, that the property * computation is allowed to query and depend on the property that initially kicked-off the * computation in the first place. '''Querying the property store may in particular be required * to identify the reason why the property was set'''. For example, if the `Caller` property * was set to the fallback due to a depending computation, it may be necessary to distinguish * between the case "no callers" and "unknown callers"; in case of the final property * "no callers" the result may very well be [[NoResult]]. * * @note A computation is guaranteed to be triggered exactly once for every e/pk pair that has * a concrete property - even if the value was already associated with the e/pk pair * before the registration is done. * * @param pk The property key. * @param pc The computation that is (potentially concurrently) called to kick-start a * computation related to the given entity. */ final def registerTriggeredComputation[E <: Entity, P <: Property]( pk: PropertyKey[P], pc: PropertyComputation[E] ): Unit = { if (!isIdle) { throw new IllegalStateException( "triggered computations can only be registered while no computations are running" ) } doRegisterTriggeredComputation(pk, pc) } protected[this] def doRegisterTriggeredComputation[E <: Entity, P <: Property]( pk: PropertyKey[P], pc: PropertyComputation[E] ): Unit /** * Will call the given function `c` for all elements of `es` in parallel. * * @see [[scheduleEagerComputationForEntity]] for details. */ final def scheduleEagerComputationsForEntities[E <: Entity]( es: IterableOnce[E] )( c: PropertyComputation[E] ): Unit = { es.iterator.foreach(e => scheduleEagerComputationForEntity(e)(c)) } /** * Schedules the execution of the given `PropertyComputation` function for the given entity. * This is of particular interest to start an incremental computation * (cf. [[IncrementalResult]]) which, e.g., processes the class hierarchy in a top-down manner. * * @note It is NOT possible to use scheduleEagerComputationForEntity for properties which * are also computed by a lazy property computation; use `force` instead! * * @note If any computation resulted in an exception, then the scheduling will fail and * the exception related to the failing computation will be thrown again. */ final def scheduleEagerComputationForEntity[E <: Entity]( e: E )( pc: PropertyComputation[E] ): Unit = { doScheduleEagerComputationForEntity(e)(pc) } protected[this] def doScheduleEagerComputationForEntity[E <: Entity]( e: E )( pc: PropertyComputation[E] ): Unit /** * Processes the result eventually; generally, not directly called by analyses. * If this function is directly called, the caller has to ensure that we don't have overlapping * results and that the given result is a meaningful update of the previous property * associated with the respective entity - if any! * * @throws IllegalStateException If the result cannot be applied. * @note If any computation resulted in an exception, then `handleResult` will fail and * the exception related to the failing computation will be thrown again. */ def handleResult(r: PropertyComputationResult): Unit /** * Executes the given function at some point between now and the return of a subsequent call * of waitOnPhaseCompletion. */ def execute(f: => Unit): Unit /** * Awaits the completion of all property computations which were previously scheduled. * As soon as all initial computations have finished, dependencies on E/P pairs for which * no value was computed, will be identified and the fallback value will be used. After that, * the remaining intermediate values will be made final. * * @note If a computation fails with an exception, the property store will stop in due time * and return the thrown exception. No strong guarantees are given which exception * is returned in case of concurrent execution with multiple exceptions. * @note In case of an exception, the analyses are aborted as fast as possible and the * store is no longer usable. */ def waitOnPhaseCompletion(): Unit /** ONLY INTENDED TO BE USED BY TESTS TO AVOID MISGUIDING TEST REPORTS! */ private[fpcf] var suppressError: Boolean = false /** * Called when the first top-level exception occurs. * Intended to be overridden by subclasses. */ protected[this] def onFirstException(t: Throwable): Unit = { doTerminate = true shutdown() if (!suppressError) { val storeId = "PropertyStore@"+System.identityHashCode(this).toHexString error( "analysis progress", s"$storeId: shutting down computations due to failing analysis", t ) } } @volatile protected[this] var exception: Throwable = _ /*null*/ protected[fpcf] def collectException(t: Throwable): Unit = { if (exception != null) { if (exception != t && !t.isInstanceOf[InterruptedException] && !t.isInstanceOf[RejectedExecutionException] // <= used, e.g., by a ForkJoinPool ) { exception.addSuppressed(t) } } else { // Here, we use double-checked locking... we don't care about performance if // everything falls apart anyway. this.synchronized { if (exception ne null) { if (exception != t) { exception.addSuppressed(t) } } else { exception = t onFirstException(t) } } } } @inline protected[fpcf] def collectAndThrowException(t: Throwable): Nothing = { collectException(t) throw t; } @inline /*visibility should be package and subclasses*/ def handleExceptions[U](f: => U): U = { if (exception != null) throw exception; try { f } catch { case ct: ControlThrowable => throw ct; case t: Throwable => collectAndThrowException(t) } } } /** * Manages general configuration options. Please note, that changes of these options * can be done at any time. */ object PropertyStore { final val DebugKey = "org.opalj.fpcf.PropertyStore.Debug" private[this] var debug: Boolean = { val initialDebug = BaseConfig.getBoolean(DebugKey) updateDebug(initialDebug) initialDebug } /** * Determines if newly created property stores are created with debug turned on or off. * * Does NOT affect existing instances! */ def Debug: Boolean = debug /** * Determines if new `PropertyStore` instances run with debugging or without debugging. * */ def updateDebug(newDebug: Boolean): Unit = { implicit val logContext: LogContext = GlobalLogContext debug = if (newDebug) { info("OPAL - new PropertyStores", s"$DebugKey: debugging support on") true } else { info("OPAL - new PropertyStores", s"$DebugKey: debugging support off") false } } // // The following settings are primarily about comprehending analysis results than // about debugging analyses. // final val TraceFallbacksKey = "org.opalj.fpcf.PropertyStore.TraceFallbacks" private[this] var traceFallbacks: Boolean = { val initialTraceFallbacks = BaseConfig.getBoolean(TraceFallbacksKey) updateTraceFallbacks(initialTraceFallbacks) initialTraceFallbacks } // We think of it as a runtime constant (which can be changed for testing purposes). def TraceFallbacks: Boolean = traceFallbacks def updateTraceFallbacks(newTraceFallbacks: Boolean): Unit = { implicit val logContext: LogContext = GlobalLogContext traceFallbacks = if (newTraceFallbacks) { info( "OPAL - new PropertyStores", s"$TraceFallbacksKey: usages of fallbacks are reported" ) true } else { info( "OPAL - new PropertyStores", s"$TraceFallbacksKey: fallbacks are not reported" ) false } } final val TraceSuppressedNotificationsKey = { "org.opalj.fpcf.PropertyStore.TraceSuppressedNotifications" } private[this] var traceSuppressedNotifications: Boolean = { val initialTraceSuppressedNotifications = BaseConfig.getBoolean(TraceSuppressedNotificationsKey) updateTraceDependersNotificationsKey(initialTraceSuppressedNotifications) initialTraceSuppressedNotifications } // We think of it as a runtime constant (which can be changed for testing purposes). def TraceSuppressedNotifications: Boolean = traceSuppressedNotifications def updateTraceDependersNotificationsKey(newTraceSuppressedNotifications: Boolean): Unit = { implicit val logContext: LogContext = GlobalLogContext traceSuppressedNotifications = if (newTraceSuppressedNotifications) { info( "OPAL - new PropertyStores", s"$TraceSuppressedNotificationsKey: suppressed notifications are reported" ) true } else { info( "OPAL - new PropertyStores", s"$TraceSuppressedNotificationsKey: suppressed notifications are not reported" ) false } } }