All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.opalj.fpcf.AnalysisScenario.scala Maven / Gradle / Ivy

The newest version!
/* BSD 2-Clause License - see OPAL/LICENSE for details. */
package org.opalj
package fpcf

import org.opalj.log.LogContext
import org.opalj.log.OPALLogger
import org.opalj.graphs.Graph

/**
 * Provides functionality to determine whether a set of analyses is compatible and to compute
 * a schedule to execute a set of analyses.
 *
 * @author Michael Eichberg
 */
class AnalysisScenario[A](val ps: PropertyStore) {

    private[this] var scheduleComputed: Boolean = false

    private[this] var allCS: Set[ComputationSpecification[A]] = Set.empty

    private[this] var derivedProperties: Set[PropertyBounds] = Set.empty
    private[this] var eagerlyDerivedProperties: Set[PropertyBounds] = Set.empty
    private[this] var collaborativelyDerivedProperties: Set[PropertyBounds] = Set.empty
    private[this] var lazilyDerivedProperties: Set[PropertyBounds] = Set.empty

    private[this] var initializationData: Map[ComputationSpecification[A], Any] = Map.empty

    private[this] var usedProperties: Set[PropertyBounds] = Set.empty

    private[this] var eagerCS: Set[ComputationSpecification[A]] = Set.empty
    private[this] var lazyCS: Set[ComputationSpecification[A]] = Set.empty
    private[this] var triggeredCS: Set[ComputationSpecification[A]] = Set.empty
    private[this] var transformersCS: Set[ComputationSpecification[A]] = Set.empty

    private[this] var derivedBy: Map[PropertyKind, (PropertyBounds, Set[ComputationSpecification[A]])] = {
        Map.empty
    }

    def allProperties: Set[PropertyBounds] = derivedProperties ++ usedProperties

    /**
     * Adds the given computation specification (`cs`) to the set of computation specifications
     * that should be scheduled.
     */
    def +=(cs: ComputationSpecification[A]): this.type = {
        if (scheduleComputed) {
            throw new IllegalStateException("process was already computed");
        }

        cs.computationType match {
            case EagerComputation     => eagerCS += cs
            case TriggeredComputation => triggeredCS += cs
            case LazyComputation      => lazyCS += cs
            case Transformer          => transformersCS += cs
        }

        allCS += cs

        initializationData += cs -> cs.init(ps)

        this
    }

    /**
     * Returns the graph which depicts the dependencies between the computed properties
     * based on the current computation specifications.
     * I.e., a property `d` depends on another property `p` if the algorithm which computes
     * `d` uses the property `p`.
     *
     * @note Can only be called after the schedule method was called! If no schedule could be
     *       computed the result of this method is undefined.
     */
    def propertyComputationsDependencies: Graph[PropertyBounds] = {
        if (!scheduleComputed) {
            throw new IllegalStateException("initialization incomplete; schedule not computed");
        }
        val psDeps = Graph.empty[PropertyBounds]
        allCS foreach { cs =>
            // all derived properties depend on all used properties
            cs.derives foreach { derived =>
                psDeps addVertice derived
                cs.uses(ps) foreach { use => psDeps addEdge (derived, use) }
            }
        }
        psDeps
    }

    /**
     * Returns the dependencies between the computations.
     *
     * @note Can only be called after the schedule method was called! If no schedule could be
     *       computed the result of this method is undefined.
     */
    def computationDependencies: Graph[ComputationSpecification[A]] = {
        if (!scheduleComputed) {
            throw new IllegalStateException("initialization incomplete; schedule not computed");
        }

        val compDeps = Graph.empty[ComputationSpecification[A]]
        val derivedBy: Map[PropertyBounds, Set[ComputationSpecification[A]]] = {
            var derivedBy: Map[PropertyBounds, Set[ComputationSpecification[A]]] = Map.empty
            allCS foreach { cs =>
                cs.derives foreach { derives =>
                    derivedBy += derives -> (derivedBy.getOrElse(derives, Set.empty) + cs)
                }
            }
            derivedBy
        }
        allCS foreach { cs =>
            compDeps addVertice cs
            cs.uses(ps) foreach { usedPK =>
                derivedBy.get(usedPK).iterator.flatten.foreach { providerCS =>
                    if (providerCS ne cs) {
                        compDeps addEdge (cs, providerCS)
                    }
                }
            }
        }
        // let's handle the case that multiple analyses derive a property collaboratively
        derivedBy.valuesIterator.filter(_.size > 1) foreach { css =>
            val cssIt = css.iterator
            val headCS = cssIt.next()
            var lastCS = headCS
            do {
                val nextCS = cssIt.next()
                compDeps addEdge (lastCS -> nextCS)
                lastCS = nextCS
            } while (cssIt.hasNext)
            compDeps addEdge (lastCS -> headCS)
        }

        compDeps
    }

    private[this] def processCS(cs: ComputationSpecification[A]): Unit = {
        // 1. check the most basic constraints
        cs.derivesLazily foreach { lazilyDerivedProperty =>
            if (derivedProperties.contains(lazilyDerivedProperty)) {
                val pkName = PropertyKey.name(lazilyDerivedProperty.pk.id)
                val m = s"can not register $cs: $pkName is already computed by another analysis"
                throw new SpecificationViolation(m)
            }
        }

        cs.derivesCollaboratively foreach { collaborativelyDerivedProperty =>
            if (eagerlyDerivedProperties.contains(collaborativelyDerivedProperty) ||
                lazilyDerivedProperties.contains(collaborativelyDerivedProperty)) {
                val pkName = PropertyKey.name(collaborativelyDerivedProperty.pk.id)
                val m =
                    s"can not register $cs: "+
                        s"$pkName is not computed collaboratively by all analyses"
                throw new SpecificationViolation(m)
            }
        }

        cs.derivesEagerly foreach { eagerlyDerivedProperty =>
            if (derivedProperties.contains(eagerlyDerivedProperty)) {
                val pkName = PropertyKey.name(eagerlyDerivedProperty.pk.id)
                val m = s"can not register $cs: $pkName is already computed by another analysis"
                throw new SpecificationViolation(m)
            }
        }

        // TODO Check inner consistency: that is, if an analysis derives multiple properties, they have to be compatible

        // 2. register the analysis
        def handleDerivedProperties(derivedProperties: Set[PropertyBounds]): Unit = {
            this.derivedProperties ++= derivedProperties
            derivedProperties foreach { derivedProperty =>
                val pk = derivedProperty.pk
                derivedBy.get(pk) match {
                    case None =>
                        derivedBy += ((pk, (derivedProperty, Set(cs))))
                    case Some((`derivedProperty`, css)) =>
                        derivedBy += ((pk, (derivedProperty, css + cs)))
                    case Some((deviatingPropertyBounds, css)) =>
                        val propertyName = PropertyKey.name(pk)
                        throw new IllegalArgumentException(
                            s"different bounds ($deviatingPropertyBounds vs. $derivedProperty) "+
                                s"are computed by $css vs. $cs "+
                                s"for the collaboratively computed property $propertyName"
                        )
                }
            }
        }

        usedProperties ++= cs.uses(ps)

        eagerlyDerivedProperties ++= cs.derivesEagerly
        handleDerivedProperties(cs.derivesEagerly)
        collaborativelyDerivedProperties ++= cs.derivesCollaboratively
        handleDerivedProperties(cs.derivesCollaboratively)
        lazilyDerivedProperties ++= cs.derivesLazily.toList
        handleDerivedProperties(cs.derivesLazily.toSet)
    }

    /**
     * Computes an executable schedule.
     *
     * When executing a schedule, the following steps will be performed:
     *  1. all analyses' init method will be called; this may lead to the initialization
     *     of properties
     *
     *  1. setupPhase is called
     *  1. all lazy analyses and all transformers are registered;
     *     (Immediately before registration, beforeSchedule is called.)
     *  1. all triggered computations are registered; this may trigger computations related
     *     to initial properties
     *     (Immediately before registration, beforeSchedule is called.)
     *  1. all eager analyses are started
     *     (Immediately before registration, beforeSchedule is called.)
     *  1. when the phase has finished, all analyses' afterPhaseCompletion methods are called.
     *
     * @param propertyStore required to determine which properties are already computed!
     */
    def computeSchedule(
        propertyStore: PropertyStore
    )(
        implicit
        logContext: LogContext
    ): Schedule[A] = {
        if (scheduleComputed) {
            throw new IllegalStateException("schedule already computed");
        } else {
            scheduleComputed = true
        }

        allCS.foreach(processCS)

        val alreadyComputedPropertyKinds = propertyStore.alreadyComputedPropertyKindIds.toSet

        // 0. check that a property was not already derived
        allCS.foreach { cs =>
            cs.derives foreach { derivedProperty =>
                if (alreadyComputedPropertyKinds.contains(derivedProperty.pk.id)) {
                    val pkName = PropertyKey.name(derivedProperty.pk.id)
                    val m = s"can not register $cs: $pkName was computed in a previous phase"
                    throw new SpecificationViolation(m)
                }
            }
        }

        // 1. check for properties that are not derived (and which require an analysis)
        val underivedProperties = usedProperties -- derivedProperties
        underivedProperties
            .filterNot { underivedProperty =>
                alreadyComputedPropertyKinds.contains(underivedProperty.pk.id)
            }
            .foreach { underivedProperty =>
                val propertyName = PropertyKey.name(underivedProperty.pk.id)
                if (PropertyKey.hasFallback(underivedProperty.pk)) {
                    val message = s"no analyses scheduled for: $propertyName; using fallback"
                    OPALLogger.warn("analysis configuration", message)
                } else {
                    throw new IllegalStateException(s"no analysis scheduled for $propertyName")
                }
            }

        // TODO check all further constraints (in particular those related to cyclic dependencies between analysis...)

        // 2. assign analyses to different batches if an analysis can only process
        //    final properties (unless it is a transformer, the latter have special paths and
        //    constraints and can always be scheduled in the same batch!)

        // TODO ....

        Schedule(
            if (allCS.isEmpty) List.empty else List(computePhase(propertyStore)),
            initializationData
        )
    }

    /**
     * Computes the configuration for a specific batch; this method can only handle the situation
     * where all analyses can be executed in the same phase.
     */
    private def computePhase(propertyStore: PropertyStore): PhaseConfiguration[A] = {

        // 1. compute the phase configuration; i.e., find those properties for which we must
        //    suppress interim updates.
        var suppressInterimUpdates: Map[PropertyKind, Set[PropertyKind]] = Map.empty
        // Interim updates have to be suppressed when an analysis uses a property for which
        // the wrong bounds/not enough bounds are computed.
        transformersCS foreach { cs =>
            suppressInterimUpdates += (cs.derivesLazily.get.pk -> cs.uses(ps).map(_.pk))
        }

        // 3. create the batch
        val batchBuilder = List.newBuilder[ComputationSpecification[A]]
        batchBuilder ++= lazyCS
        batchBuilder ++= transformersCS
        batchBuilder ++= triggeredCS
        batchBuilder ++= eagerCS

        // FIXME...

        // Interim updates can be suppressed when the depender and dependee are not in a cyclic
        // relation; however, this could have a negative impact on the effect of deep laziness -
        // once we are actually implementing it. For the time being, suppress notifications is always
        // advantageous.

        val phase1Configuration = PropertyKindsConfiguration(
            propertyKindsComputedInThisPhase = derivedProperties.map(_.pk),
            suppressInterimUpdates = suppressInterimUpdates
        )

        PhaseConfiguration(phase1Configuration, batchBuilder.result())
    }
}

/**
 * Factory to create an [[AnalysisScenario]].
 */
object AnalysisScenario {

    /**
     * @param analyses The set of analyses that should be executed as part of this analysis scenario.
     */
    def apply[A](
        analyses:      Iterable[ComputationSpecification[A]],
        propertyStore: PropertyStore
    ): AnalysisScenario[A] = {
        val as = new AnalysisScenario[A](propertyStore)
        analyses.foreach(as.+=)
        as
    }

}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy