Signaux

Le moteur d'exécution et les processus sont en place et nous allons maintenant nous attaquer à l'implémentation des signaux. Pour cela, on commencera par le cas des signaux "purs" sans valeur associée, qui seront ensuite améliorés pour créer des signaux avec valeurs.

Signaux purs

On utilisera pour l'implémentation une structure de donnée partagée entre toutes les références d'un signal et qui gérera l'attente et l'émission du signal. On définit donc les types SignalRuntime ainsi que SignalRuntimeRef qui encapsule les références au SignalRuntime:


# #![allow(unused_variables)]
#fn main() {
pub struct SignalRuntime<'r> {
    // TODO: define the fields
}

pub struct SignalRuntimeRef<'r> {
    signal_runtime: Rc<SignalRuntime<'r>>,
}

impl<'r> SignalRuntimeRef<'r> {
    /// Sets the signal as emitted for the current instant.
    fn emit(&self, runtime: &mut Runtime<'r>) {
        unimplemented!()
    }

    /// Resets the signal value, clearing the emitted flag.  This is called at
    /// end of instant.
    fn reset(&self, runtime: &mut Runtime<'r>) {
        unimplemented!()
    }

    /// Register a pair of activators for the `present` construct.  The
    /// `present` activator should be called when the signal is emitted, and
    /// `not_present` should be called during the next `reset` if the signal
    /// was not emitted.
    fn register_present(
        &self,
        runtime: &mut Runtime<'r>,
        present: Activator<'r>, 
        not_present: Activator<'r>,
    ) {
        unimplemented!()
    }

    // TODO: Add other methods here
}
#}

Le SignalRuntimeRef expose des méthodes permettant d'interagir avec le signal. On vous donne la signature de deux méthodes pour émettre et réinitialiser le signal, ainsi que pour enregistrer des activateurs correspondant à la construction present de ReactiveML; à vous de rajouter des méthodes supplémentaires selon vos besoins.

Le SignalRuntimeRef ne sera pas directement exposé à l'utilisateur de la bibliothèque car on ne veut pas avoir besoin de manipuler directement des activateurs. On définit donc un trait Signal qui fournit des méthodes retournant des processus qui correspondant aux différentes constructions de ReactiveML.


# #![allow(unused_variables)]
#fn main() {
pub trait Signal<'r> {
    fn runtime(self) -> SignalRuntimeRef<'r>;
}

pub trait SignalExt {
    fn emit(self) -> Emit<Self> where Self: Sized {
        Emit { signal: self }
    }

    fn present(self) -> Present<Self> where Self: Sized {
        Present { signal: self }
    }

    // TODO: Add other methods here
}

impl<Sig> SignalExt for Sig where Sig: Signal {}

struct<Sig> Emit<Sig> {
    signal: Sig,
}

impl<Sig> ProcessOnceExt for Emit<Sig> {}

impl<'r, O: 'r, Sig: 'r> ProcessOnce<'r, O> for Emit<Sig>
where
    O: Tuple + OutputEdgeOnce<Runtime<'r>>,
    O::Item: Default,
    Sig: Signal<'r>,
{
    type Inputs = (ControlEdge<'r>, );

    fn compile_once<'a>(self, b: &mut Builder<'a, 'r>, outputs: O) -> Self::Inputs
    {
        unimplemented!()
    }
}

struct<Sig> Present<Sig> {
    signal: Sig,
}

impl<Sig> ProcessOnceExt for Present<Sig> {}

impl<'r, Sig: 'r> ProcessOnce<'r, (ControlEdge<'r>, ControlEdge<'r>)> for Present<Sig>
where
    Sig: Signal<'r>,
{
    type Inputs = (ControlEdge<'r>, );

    fn compile_once<'a>(
        self,
        b: &mut Builder<'a, 'r>,
        (present, not_present): (ControlEdge<'r>, ControlEdge<'r>),
    ) -> Self::Inputs
    {
        unimplemented!()
    }
}
#}

Question 1. Créez un object PureSignal qui implémente le trait Signal. Ce signal doit pouvoir être émit, attendu (avec la construction await immediate), et on doit pouvoir tester sa présence (avec la construction present).

Question 2. Testez votre implémentation sur le programme ci-dessous:

signal s in
loop
  emit s;
  pause;
end
||
loop
  present s then begin
    print_endline "present";
    pause;
  end else print_endline "not present";
end
||
loop
  await immediate s;
  print_endline "s received";
  pause;
end

Signaux valués

En ReactiveML la valeur d'un signal peut être lue par plusieurs processus car il s'agit en réalité d'un pointeur vers un objet géré par le ramasse-miettes. En Rust, il n'y a pas de ramasse-miettes et il faut donc soit copier la valeur, soit s'assurer que l'on ne peut pas récupérer la valeur d'un signal plusieurs fois. On distinguera donc deux types de signaux: les signaux à consommateurs multiples (qui copient), et les signaux à consomatteur unique.

Question 3. Créez un signal à consommateurs multiples. Ce signal devra supporter aumoins les constructions emit, await, await immediate et present de ReactiveML. Vous pourrez implémenter un ReceiverOnce pour le signal et réutiliser la construction Recv définie plus haut pour le transformer en un processus.

Question 4 (optionnel). Créez un signal à consommateur unique. Pour vous assurer de l'unicité du consommateur, vous pouvez vous appuyer sur le système de types de Rust, par exemple en utilisant une approche similaire à celle des files de messages de la bibliothèque standard.

Configurations événementielles

On souhaite maintenant ajouter la possibilité d'attendre sur plusieurs événements à la fois, ou sur l'un ou l'autre de deux événements. Ceci correspond aux configurations événementielles de ReactiveML.

Cette partie est optionnelle et n'est fournie que pour donner des pistes sur l'implémentation des configurations événementielles. Il est plus important que vous ayez bien compris les parties précédentes que de s'attaquer à cette partie.

Question 5. En vous inspirant du SignalRuntime, créez une structure AndRuntime qui pointe vers deux SignalRuntime et fournit les méthodes emit, cancel et reset. emit sera appelé par chacun des SignalRuntime quand ils sont émis (il faut donc deux appels à emit avant d'activer les tâches en attente), tandis que cancel sera appelé en fin d'instant par les SignalRuntime si ils avaient été émis durant l'instant. Enfin reset sera appelé en fin d'instant, comme pour le SignalRuntime. Il peut être judicieux de créer un trait Resettable pour stocker les signaux et configurations ensemble dans le moteur d'exécution.

Question 6. En vous inspirant de la question précédente, créez une structure OrRuntime fournissant les même méthodes que AndRuntime, mais qui active les processus en attente dès réception de l'un ou l'autre des signaux. OrRuntime doit permettre d'enregistrer deux processus différents à exécuter selon le signal qui a été émis (attention: un seul de ces processus doit être exécuté, même quand les deux signaux sont émis ! Pour respecter les hypothèses de synchronicité, le processus exécuté ne doit pas dépendre de l'ordre d'émission des signaux).

Question 7. Implémentez des configurations événementielles valuées en vous basant sur AndRuntime et OrRuntime.

On peut maintenant implémenter les constructions avancées du langage ReactiveML comme le do .. when s ou le do .. until s en se basant sur ces configurations événementielles. On pourra considérer l'existence d'un signal spécial tick, toujours présent et automatiquement émis par le moteur d'exécution en début d'instant (ainsi la construction pause par exemple est équivalente à await tick).

On peut alors transformer les do .. when s an ajoutant le signal s à toutes les instructions await (et en transformant donc pause en await tick /\ s) et en introduisant un await immediate s en début du corps. Similairement, le do .. until s -> e2 s'implémente en ajoutant une disjonction: par exemple, à l'intérieur du corps, pause; e1 devient await (s -> e2) \/ (tick -> e1) en supposant que la première branche du await est préférée.

Question 8. En vous basant sur le trait ProcessOnce, créez des traits ProcessOnceWhen et ProcessOnceUntil permettant la compilation de leur corps à l'intérieur d'un do .. when et d'un do .. until respectivement.