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.