
org.opalj.fpcf.PropertyStore.scala Maven / Gradle / Ivy
The newest version!
/* 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
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy