CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
Solution des exercices du cours sur machine du 22 décembre 2023

Métamodèle des automates

En représentant la propriété « avoir un nom » par une classe abstraite NamedEntity, on obtient le métamodèle suivant :

Sémantique statique

Pour s'assurer qu'un automate a bien un unique état initial (ce qui n'est pas garanti ici par la syntaxe abstraite), nous utilisons un invariant OCL. Cet invariant peut-être placé dans le métamodèle en l'ouvrant avec l'éditeur OCLinEcore :

package fSM : fSM = 'http://www.example.org/fSM'
{
	abstract class NamedEntity
	{
		attribute name : String[1];
	}
	class StateMachine extends NamedEntity
	{
		property states : State[+|1] { ordered composes };
		property inputs : Input[*|1] { ordered composes };
		property outputs : Output[*|1] { ordered composes };
		invariant oneInitialState: states->select(isInitial)->size() = 1;
	}
	class State extends NamedEntity
	{
		property transitions : Transition[*|1] { ordered composes };
		attribute isInitial : Boolean[1];
	}
	class Input extends NamedEntity;
	class Output extends NamedEntity;
	class Transition
	{
		property trigger : Input[?];
		property actions : Output[*|1] { ordered };
		property target : State[1];
	}
}

L'invariant se trouve ligne 12, dans le contexte de la classe StateMachine. Son nom est oneInitialState et l'expression OCL qui doit être vérifiée est states->select(isInitial)->size() = 1

Cette expression exprime que la taille de la collection obtenue en sélectionnant parmi les State de la collection states d'une StateMachine, ceux dont l'attribut isInitial est vrai, doit être égale à 1.

Cet invariant, ainsi que les contraintes liées à l'arité des relations entre éléments du modèle, est vérifié lors que l'on valide un modèle (en sélectionnant l'élément Validate du menu contextuel des modèles).

Métamodèle pour la génération de code

Ce métamodèle est moins abstrait et beaucoup plus proche de l'implémentation. Les entrées et sorties de l'automate sont ici des chaînes de caractères, et il n'y a pas de contrainte pour imposer que la garde d'une transition ou ses actions sont bien des entrées et des sorties de l'automate, contrairement à ce que permettaient les classes Input et Output du métamodèle précédent. Ceci n'est toutefois pas grave si ce métamodèle n'est utilisé qu'en tant que cible d'une transformation, et pas pour créer directement des modèles d'automates. La transformation partira d'un modèle où ces contraintes sont vérifiées, et, si elle est correcte, génèrera une instance correcte de ce métamodèle.

Transformation FSM vers CodeFSM

Il s'agit ici d'une transformation Model to model, que nous modélisons avec QVT operational :

modeltype SRC "strict" uses fSM('http://www.example.org/fSM');
modeltype DST "strict" uses codeFSM('http://www.example.org/codeFSM');

transformation FSM2Code(in input : SRC, out output : DST);

main() {
	input.rootObjects()[SRC::StateMachine]->toCode();
}

mapping SRC::StateMachine::toCode() : DST::StateMachine {
	assert fatal (self.states->select(isInitial)->size() = 1)
		with log('A state machine should have exactly one initial state');

	result.name := self.name;
	result.inputs := self.inputs->collect(i|i.name);
	result.outputs := self.outputs->collect(o|o.name);
	result.states := self.states->toCode();
	result.initialState := self.states->select(s|s.isInitial).resolveoneIn(State::toCode);
}

mapping SRC::State::toCode() : DST::State {
	result.name := self.name;
	result.transitions := self.transitions->toCode();
}

mapping SRC::Transition::toCode() : DST::Transition {
	result.trigger := self.trigger.name;
	result.actions := self.actions->collect(o|o.name);
	result.target := self.target.late resolveoneIn(State::toCode);
}

On notera, ligne 7, la restriction des objets racine à traiter à ceux de type StateMachine du métamodèle source (type placé entre crochets après l'appel à rootObjects()). Cette restriction n'est pas indispensable mais nous protège contre des modèles mal formés, ou contre l'oubli de la mise à jour de la transformation en cas de modification du métamodèle. Il s'agit donc d'une forme de programmation défensive (prévoir même les cas a priori impossibles).

La fonction OCL collect (voir le memento OCL) est utilisée pour construire une nouvelle collection à partir d'une expression évaluée pour chaque élément d'une collection. Ainsi self.inputs->collect(i|i.name) construit la collection des noms des Inputs d'une StateMachine à la la ligne 15. Quand l'expression est simplement l'accès à un attribut, il est possible d'abréger, on aurait ici pu écrire : self.inputs->collect(name).

La fonction OCL select est utilisée pour construire la collection des éléments d'une collection qui satisfont une expression booléenne. Ainsi, self.states->select(s|s.isInitial) construit la collection des états initiaux d'une StateMachine à la ligne 18. De même que pour collect, on aurait pu écrire ici : self.states->select(isInitial).

La fonction resolveoneIn de QVT operational retrouve l'image par un mapping d'un élément du modèle de départ dans le modèle résultat. Ainsi, à la ligne 18, on retrouve l'image de l'état initial de l'automate par le mapping entre états des modèles source et destination.

Le mot-clef late avant une instruction de résolution indique que la résolution doit être retardée après la création des éléments du modèle résultat. Ceci est nécessaire ligne 29 car le mapping des transitions se fait au cours du mapping des états, et l'état cible d'une transition n'a pas nécessairement été créé au moment où on la traite. Au contraire, ligne 18, il n'est pas nécessaire d'utiliser late car la création des états dans le modèle résultat a été faite à la ligne 17 par self.states->toCode().

Lignes 11 et 12, une assertion arrête la transformation au cas où le nombre d'états initiaux est différent de 1, en affichant un message d'erreur.

Les différents mappings s'appliquent aux différents types d'éléments du modèle, et dans chaque mapping, la variable result correspond à l'élément créé dans le modèle résultat, tandis que la variable self correspond à l'élément du modèle source que l'on traite. Il est important de bien initialiser tous les attributs de result.

Génération du fichier HTML/JavaScript

Après avoir créé le projet Acceleo pour cette transformation, collez le code que vous voulez générer dans le template [file ...] ... [/file], et corrigez les éventuelles erreurs provoquées par l'existence de caractères [ dans le code à générer (il faut les remplacer par ['['/]). Ensuite, identifiez les parties du code qui dépendent du modèle source, et remplacez les par des instructions Acceleo qui naviguent dans le modèle et utilisent ses éléments.

Une solution possible pour notre transformation est :

[comment encoding = UTF-8 /]
[module generate('http://www.example.org/codeFSM')]


[template public generateElement(aStateMachine : StateMachine)]
[comment @main/]
[file (aStateMachine.name.concat('.html'), false, 'UTF-8')]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>[aStateMachine.name/]</title>

<style>
  * {
    font-family: sans-serif;
    font-size: 100%;
  }

  div.inputs:before {
    display: inline-block;
    content: "Inputs: ";
    font-weight: bold;
    font-size: 110%;
    width: 5em;
  }

  div.outputs:before {
    display: inline-block;
    content: "Outputs: ";
    font-weight: bold;
    font-size: 110%;
    width: 5em;
  }

  div.states:before {
    display: inline-block;
    content: "State: ";
    font-weight: bold;
    font-size: 110%;
    width: 5em;
  }

  div.outputs {
    padding-top: 2ex;
  }

  div.outputs > div.buttons {
    font-size: 95%;
  }

  div.buttons {
    display: inline;
    border-style: solid;
    border-width: 1px;
    border-radius: 7px;
    padding: 5px;
  }

  div.buttons input['['/]type="button"] {
    font-size: 100%;
  }

  div.states {
    padding-top: 2ex;
  }

  div.states > div.buttons {
    font-size: 95%;
  }
</style>

<script>
function react(msg) {
[for (o : String | aStateMachine.outputs)]
  var out_[o/] = document.getElementById("out_[o/]");
  out_[o/].checked = false;
[/for]
[for (s : State | aStateMachine.states)]
  var state_[s.name/] = document.getElementById("state_[s.name/]");
[/for]
[for (s : State | aStateMachine.states)]
  if (state_[s.name/].checked) {
[for (t : Transition | s.transitions)]
    [if (not t.trigger.oclIsUndefined())]if (msg == "[t.trigger/]") [/if]{
      // Actions
[for (act : String | t.actions)]
      out_[act/].checked = true;
[/for]
      // Target state
      state_[t.target.name/].checked = true;
      return;
    }
[/for]
  }
[/for]
}
</script>
</head>

<body>

<div class="inputs">
  <div class="buttons">
    <input type="button" id="inp_void" value=" " onclick="react('')"/>
[for (inp : String | aStateMachine.inputs)]
    <input type="button" id="inp_[inp/]" value="[inp/]" onclick="react('[inp/]')"/>
[/for]
  </div>
</div>

<div class="outputs">
  <div class="buttons">
[for (out : String | aStateMachine.outputs)]
    <input type="checkbox" disabled id="out_[out/]"/>[out/]
[/for]
  </div>
</div>

<div class="states">
  <div class="buttons">
[for (s : State | aStateMachine.states)]
    <input type="radio" name="state" disabled [if (s = aStateMachine.initialState)]checked [/if]id="state_[s.name/]"/>[s.name/]
[/for]
  </div>
</div>

</body>
</html>
[/file]
[/template]