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

rescala.core.Core.scala Maven / Gradle / Ivy

package rescala.core

import rescala.operator.RExceptions

import scala.annotation.{implicitNotFound, nowarn}
import scala.util.DynamicVariable

trait Core {

  /** In case you wondered why everything in REScala is in these weird bundle traits, this is why.
    * The ReSource below depends on some abstract state, which is defined by the concrete scheduler implementations.
    * As basically everything else references ReSources, everything must be bundled together.
    * This is good for users, because they get strong guarantees about the used correctness, and the API is still OK.
    * Its terrible for us, because the Scala Incremental compiler does not really work anymore.
    */
  type State[_]

  /** Source of (reactive) values. */
  trait ReSource {
    type Value
    protected[rescala] def state: State[Value]
    protected[rescala] def name: ReName
    protected[rescala] def commit(base: Value): Value
  }

  /** A reactive value is something that can be reevaluated */
  trait Derived extends ReSource {

    final type ReIn = ReevTicket[Value]
    final type Rout = Result[Value]

    /** called if any of the dependencies ([[rescala.core.Core.ReSource]]s) changed in the current update turn,
      * after all (known) dependencies are updated
      */
    protected[rescala] def reevaluate(input: ReIn): Rout
  }

  /** Base implementation for reactives, with [[Derived]] for scheduling,
    * together with a [[ReName]] and containing a [[State]]
    *
    * @param state the initial state passed by the scheduler
    * @param name  the name of the reactive, useful for debugging as it often contains positional information
    */
  abstract class Base[V](override protected[rescala] val state: State[V], override val name: ReName)
      extends ReSource {
    override type Value = V
    override def toString: String = s"${name.str}($state)"
  }

  /** Common macro accessors for [[rescala.operator.SignalBundle.Signal]] and [[rescala.operator.EventBundle.Event]]
    *
    * @tparam A return type of the accessor
    * @groupname accessor Accessor and observers
    */
  trait ReadAs[+A] extends ReSource {

    /** Interprets the internal type to the external type
      *
      * @group internal
      */
    def read(v: Value): A
  }

  /** Encapsulates an action changing a single source. */
  trait InitialChange {

    /** The source to be changed. */
    val source: ReSource

    /** @param base         the current (old) value of the source.
      * @param writeCallback callback to apply the new value, executed only if the action is approved by the source.
      * @return the propagation status of the source (whether or not to reevaluate output reactives).
      */
    def writeValue(base: source.Value, writeCallback: source.Value => Unit): Boolean
  }

  /** An initializer is the glue between that binds the creation of the reactive from the operator and scheduler side together.
    * The operator provides the logic to wrap a state and the scheduler provides the implementation of that state.
    * This is where the two are joined. After that, the new reactive may have to be initialized.
    */
  trait Initializer {

    /** Creates and correctly initializes new [[Derived]]s */
    final private[rescala] def create[V, T <: Derived](
        incoming: Set[ReSource],
        initValue: V,
        needsReevaluation: Boolean
    )(instantiateReactive: State[V] => T): T = {
      val state    = makeDerivedStructState[V](initValue)
      val reactive = instantiateReactive(state)
      register(reactive)
      initialize(reactive, incoming, needsReevaluation)
      reactive
    }

    /** hook for schedulers to globally collect all created resources, usually does nothing */
    protected[this] def register(@nowarn("msg=never used") reactive: ReSource): Unit = ()

    /** Correctly initializes [[ReSource]]s */
    final private[rescala] def createSource[V, T <: ReSource](
        intv: V
    )(instantiateReactive: State[V] => T): T = {
      val state    = makeSourceStructState[V](intv)
      val reactive = instantiateReactive(state)
      register(reactive)
      reactive
    }

    /** Creates the internal state of [[Derived]]s */
    protected[this] def makeDerivedStructState[V](initialValue: V): State[V]

    /** Creates the internal state of [[ReSource]]s */
    protected[this] def makeSourceStructState[V](initialValue: V): State[V] =
      makeDerivedStructState[V](initialValue)

    /** to be implemented by the propagation algorithm, called when a new reactive has been instantiated and needs to be connected to the graph and potentially reevaluated.
      *
      * @param reactive          the newly instantiated reactive
      * @param incoming          a set of incoming dependencies
      * @param needsReevaluation true if the reactive must be reevaluated at creation even if none of its dependencies change in the creating turn.
      */
    protected[this] def initialize(
        reactive: Derived,
        incoming: Set[ReSource],
        needsReevaluation: Boolean
    ): Unit

  }

  /** User facing low level API to access values in a static context. */
  sealed abstract class StaticTicket(val tx: Transaction) {
    private[rescala] def collectStatic(reactive: ReSource): reactive.Value
    final def dependStatic[A](reactive: ReadAs[A]): A = reactive.read(collectStatic(reactive))
  }

  /** User facing low level API to access values in a dynamic context. */
  abstract class DynamicTicket(tx: Transaction) extends StaticTicket(tx) {
    private[rescala] def collectDynamic(reactive: ReSource): reactive.Value
    final def depend[A](reactive: ReadAs[A]): A = reactive.read(collectDynamic(reactive))
  }

  trait AccessHandler {
    // schedulers implement these to allow access
    def staticAccess(reactive: ReSource): reactive.Value
    def dynamicAccess(reactive: ReSource): reactive.Value
  }

  /** [[ReevTicket]] is given to the [[Derived]] reevaluate method and allows to access other reactives.
    * The ticket tracks return values, such as dependencies, the value, and if the value should be propagated.
    * Such usages make it unsuitable as an API for the user, where [[StaticTicket]] or [[DynamicTicket]] should be used instead.
    */
  final class ReevTicket[V](tx: Transaction, private var _before: V, accessHandler: AccessHandler)
      extends DynamicTicket(tx)
      with Result[V] {

    private var collectedDependencies: Set[ReSource] = null

    // dependency tracking accesses
    private[rescala] override def collectStatic(reactive: ReSource): reactive.Value = {
      assert(collectedDependencies == null || collectedDependencies.contains(reactive))
      accessHandler.staticAccess(reactive)
    }

    private[rescala] override def collectDynamic(reactive: ReSource): reactive.Value = {
      assert(collectedDependencies != null, "may not access dynamic dependencies without tracking dependencies")
      val updatedDeps = collectedDependencies + reactive
      if (updatedDeps eq collectedDependencies) {
        accessHandler.staticAccess(reactive)
      } else {
        collectedDependencies = updatedDeps
        accessHandler.dynamicAccess(reactive)
      }
    }

    // inline result into ticket, to reduce the amount of garbage during reevaluation
    private var _propagate          = false
    private var value: V            = _
    private var effect: Observation = null
    override def toString: String =
      s"Result(value = $value, propagate = $activate, deps = $collectedDependencies)"
    def before: V = _before

    /** Advises the ticket to track dynamic dependencies.
      * The passed initial set of dependencies may be processed as if they were static,
      * and are also returned in the resulting dependencies.
      */
    def trackDependencies(initial: Set[ReSource]): ReevTicket[V] = { collectedDependencies = initial; this }
    def trackStatic(): ReevTicket[V]                             = { collectedDependencies = null; this }
    def withPropagate(p: Boolean): ReevTicket[V]                 = { _propagate = p; this }
    def withValue(v: V): ReevTicket[V] = {
      require(v != null, "value must not be null");
      value = v;
      _propagate = true;
      this
    }
    def withEffect(v: Observation): ReevTicket[V] = { effect = v; this }

    override def activate: Boolean                       = _propagate
    override def forValue(f: V => Unit): Unit            = if (value != null) f(value)
    override def forEffect(f: Observation => Unit): Unit = if (effect != null) f(effect)
    override def inputs(): Option[Set[ReSource]]         = Option(collectedDependencies)

    def reset[NT](nb: NT): ReevTicket[NT] = {
      _propagate = false
      value = null.asInstanceOf[V]
      effect = null
      collectedDependencies = null
      val res = this.asInstanceOf[ReevTicket[NT]]
      res._before = nb
      res
    }
  }

  /** Result of a reevaluation */
  trait Result[T] {

    /** True iff outputs must also be reevaluated, false iff the propagation ends here. */
    def activate: Boolean

    /** No-allocation accessor for the optional new value. */
    def forValue(f: T => Unit): Unit

    /** No-allocation accessor for the effect caused by the reevaluation. */
    def forEffect(f: Observation => Unit): Unit

    /** New input resources.
      * None if unchanged.
      * Otherwise a list of all input reactives to react to.
      */
    def inputs(): Option[Set[ReSource]]
  }

  /** Records side effects for later execution. */
  trait Observation { def execute(): Unit }

  /** Enables reading of the current value during admission.
    * Keeps track of written sources internally.
    */
  final class AdmissionTicket(val tx: Transaction, declaredWrites: Set[ReSource]) {

    private var _initialChanges: Map[ReSource, InitialChange]         = Map[ReSource, InitialChange]()
    private[rescala] def initialChanges: Map[ReSource, InitialChange] = _initialChanges
    private[rescala] def recordChange[T](ic: InitialChange): Unit = {
      assert(
        declaredWrites.contains(ic.source),
        "must not set a source that has not been pre-declared for the transaction"
      )
      assert(!_initialChanges.contains(ic.source), "must not admit same source twice in one turn")
      _initialChanges += ic.source -> ic
    }

    /** convenience method as many case studies depend on this being available directly on the AT */
    def now[A](reactive: ReadAs[A]): A = tx.now(reactive)

    private[rescala] var wrapUp: Transaction => Unit = null
  }

  /** Enables the creation of other reactives */
  @implicitNotFound(msg = "Could not find capability to create reactives. Maybe a missing import?")
  final class CreationTicket(val scope: ScopeSearch, val rename: ReName) {

    private[rescala] def create[V, T <: Derived](
        incoming: Set[ReSource],
        initValue: V,
        needsReevaluation: Boolean
    )(instantiateReactive: State[V] => T): T = {
      scope.embedTransaction(_.initializer.create(incoming, initValue, needsReevaluation)(instantiateReactive))
    }
    private[rescala] def createSource[V, T <: ReSource](intv: V)(instantiateReactive: State[V] => T): T = {
      scope.embedTransaction(_.initializer.createSource(intv)(instantiateReactive))
    }
  }

  object CreationTicket {
    implicit def fromScope(implicit scope: ScopeSearch, line: ReName): CreationTicket =
      new CreationTicket(scope, line)
    // cases below are when one explicitly passes one of the parameters
    implicit def fromExplicitDynamicScope(factory: DynamicScope)(implicit line: ReName): CreationTicket =
      new CreationTicket(new ScopeSearch(Right(factory)), line)
    implicit def fromTransaction(tx: Transaction)(implicit line: ReName): CreationTicket =
      new CreationTicket(new ScopeSearch(Left(tx)), line)
    implicit def fromName(str: String)(implicit scopeSearch: ScopeSearch): CreationTicket =
      new CreationTicket(scopeSearch, ReName(str))

  }

  /** Essentially a kill switch, that will remove the reactive at some point. */
  trait Disconnectable {
    def disconnect(): Unit
  }

  /** Removes the reactive instead of its next normal reevaluation.
    * This makes use of the fact, that all reactives are technically dynamic reactives,
    * and removing incoming dependencies is always kinda safe, as long as we are sure we no longer care!
    */
  trait DisconnectableImpl extends Derived with Disconnectable {
    @volatile private var disconnected = false
    final def disconnect(): Unit = {
      disconnected = true
    }

    final override protected[rescala] def reevaluate(rein: ReIn): Rout = {
      if (disconnected) {
        rein.trackDependencies(Set.empty)
        rein
      } else {
        guardedReevaluate(rein)
      }
    }

    protected[rescala] def guardedReevaluate(rein: ReIn): Rout

  }

  /** A transaction (or maybe transaction handle would be the better term) is available from reevaluation and admission tickets.
    * That is, everywhere during a transaction, you can read reactives, but also create them.
    * The reading values is core to any reactive propagation.
    * But creating reactives using the [[Initializer]] is a liability to the scheduler, but a superpower to the operators.
    * Its a classical tradeoff, but it would be better to not make this choice by default,
    * that is, reactive creation should be limited such that we can experiment with schedulers that do not have this liability.
    */
  trait Transaction {

    final def now[A](reactive: ReadAs[A]): A = {
      RExceptions.toExternalReadException(reactive, reactive.read(access(reactive)))
    }
    private[rescala] def access(reactive: ReSource): reactive.Value

    def observe(obs: Observation): Unit

    def initializer: Initializer
  }

  /** Scheduler that defines the basic data-types available to the user and creates turns for propagation handling.
    * Note: This should NOT extend [[DynamicScope]], but did so in the past and there are too many tests that assume so ...
    */
  @implicitNotFound(msg = "Could not find an implicit scheduler. Did you forget an import?")
  trait Scheduler extends DynamicScope {
    final def forceNewTransaction[R](initialWrites: ReSource*)(admissionPhase: AdmissionTicket => R): R = {
      forceNewTransaction(initialWrites.toSet, admissionPhase)
    }
    def forceNewTransaction[R](initialWrites: Set[ReSource], admissionPhase: AdmissionTicket => R): R
    private[rescala] def singleReadValueOnce[A](reactive: ReadAs[A]): A

    /** Name of the scheduler, used for helpful error messages. */
    def schedulerName: String
    override def toString: String = s"Scheduler($schedulerName)"

    def maybeTransaction: Option[Transaction]
  }

  /** Provides the capability to look up transactions in the dynamic scope. */
  trait DynamicScope {
    private [rescala] def dynamicTransaction[T](f: Transaction => T): T
    def maybeTransaction: Option[Transaction]
  }

  trait SchedulerImpl[Tx <: Transaction] extends DynamicScope with Scheduler {

    final private[rescala] def dynamicTransaction[T](f: Transaction => T): T = {
      _currentTransaction.value match {
        case Some(transaction) => f(transaction)
        case None              => forceNewTransaction(Set.empty, ticket => f(ticket.tx))
      }
    }

    final protected val _currentTransaction: DynamicVariable[Option[Tx]] =
      new DynamicVariable[Option[Tx]](None)
    final private[rescala] def withDynamicInitializer[R](init: Tx)(thunk: => R): R =
      _currentTransaction.withValue(Some(init))(thunk)

    final override def maybeTransaction: Option[Transaction] = {
      _currentTransaction.value
    }
  }

  case class ScopeSearch(self: Either[Transaction, DynamicScope]) {

    /** Either just use the statically found transaction,
      * or do a lookup in the dynamic scope.
      * If the lookup fails, it will start a new transaction.
      */
    def embedTransaction[T](f: Transaction => T): T =
      self match {
        case Left(integrated) => f(integrated)
        case Right(ds)        => ds.dynamicTransaction(dt => f(dt))
      }

    def maybeTransaction: Option[Transaction] = self match {
      case Left(integrated) => Some(integrated)
      case Right(ds)        => ds.maybeTransaction
    }

  }

  /** As reactives can be created during propagation, any Ticket can be converted to a creation ticket. */
  object ScopeSearch extends LowPriorityScopeImplicits {

    implicit def fromTicketImplicit(implicit ticket: StaticTicket): ScopeSearch =
      new ScopeSearch(Left(ticket.tx))
    implicit def fromAdmissionImplicit(implicit ticket: AdmissionTicket): ScopeSearch =
      new ScopeSearch(Left(ticket.tx))
    implicit def fromTransactionImplicit(implicit tx: Transaction): ScopeSearch =
      new ScopeSearch(Left(tx))
  }

  /** If no Fitting Ticket is found, then these implicits will search for a [[DynamicScope]],
    * creating the reactives outside of any turn.
    */
  sealed trait LowPriorityScopeImplicits {
    implicit def fromSchedulerImplicit(implicit factory: DynamicScope): ScopeSearch =
      new ScopeSearch(Right(factory))
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy