Grammaire XText
grammar org.xtext.example.mydsl.MyDsl with org.eclipse.xtext.common.Terminals generate myDsl "http://www.xtext.org/example/mydsl/MyDsl" Model: name = ID 'inputs:' inputs += Input* 'outputs:' initializations += Initialization* 'rules:' rules += Rule* ; Input: name = ID ; Initialization: output = Output '=' value = Bool ; Output: name = ID ; Bool: isTrue ?= 'true' | {Bool} 'false' ; Rule: guard = Guard trigger = [Input] '->' actions += Action* ; Guard: {Guard} '[' ((isNeg ?= '!')? output = [Output])? ']' ; Action: output = [Output] '=' value = Bool ;
Quelques rappels sur le fonctionnement de XText
Chaque non terminal correspondra à une classe du métamodèle du langage, chaque nom utilisé pour stocker les éléments sera un attribut de cette classe. Il existe trois opérateurs pour stocker les éléments reconnus par la grammaire dans les attributs :
=
stocke simplement l'élément dans l'attribut, c'est le cas de l'identificateur (chaîne de caractère) dans l'attributname
d'unModel
.+=
ajoute l'élément reconnu à une séquence d'éléments. C'est l'opérateur à appliquer lorsqu'on utilise les expressions régulières*
(zéro ou plusieurs éléments) et+
(au moins un élément). Ainsi, l'attributinputs
d'unModel
sera une séquence d'objets de typeInput
?=
donne la valeurtrue
à l'attribut si la règle de grammaire s'applique. Cet opérateur permet de savoir si un élément optionnel a été reconnu, ou quelle branche d'une alternative a été prise. Il est utilisé ici pour capturer la négation d'une garde dansGuard
, et pour détecter la reconnaissance du mot-cleftrue
dansMyBoolean
.
La création d'une instance d'une classe du métamodèle ne se fait que lorsqu'un attribut de l'objet reçoit une valeur, il est parfois nécessaire de forcer la création de l'objet en plaçant son type entre accolade. C'est le rôle de {Guard}
dans le cas d'une garde vide. Sans cela, la règle ne rendrait rien lorsque []
est reconnu, et ne rendrait une instance de Guard
que lorsque la garde n'est pas vide. Il faudrait par exemple tester l'attribut guard
d'une Rule
avec oclIsUndefined
pour savoir si la garde est définie ou pas.
L'attribut name
est spécial pour XText qui le considère comment un identifiant de l'objet. Deux objets ayant la même valeur de name
sont considérés comme égaux.
Points importants dans cette grammaire
On remarque qu'un Input
et un Output
ont la même structure syntaxique, pourtant, ils sont définis comme deux non-terminaux distincts. Ceci permet de faire la différence entre les identificateurs qui ont été reconnus dans le contexte d'une déclaration d'inputs et ceux qui ont été reconnus dans le contexte d'une déclaration d'outputs. On dit que les notions d'Input et d'Output sont réifiées (transformées en objet), ce qui ne serait pas le cas si on ne considérait que des ID.
Cette information sémantique est utilisée dans les non-terminaux Rule
et Action
pour indiquer que l'attribut trigger
d'une Rule
doit être un identifiant qui correspond à un Input
déjà rencontré. De même dans Action
, l'identifiant de l'output doit avoir été rencontré dans une déclaration d'output. Les crochets carrés [
et ]
indiquent que l'élément reconnu à cet endroit doit déjà faire partie du modèle. L'attribut correspondant sera donc une simple référence (pas de composition), ce que vous pouvez vérifier en ouvrant le métamodèle généré par XText (il se trouve dans le sous-dossier generated
du dossier model
du projet XText) et en regardant la valeur de l'option containment
des attributs correspondants.
Cette information est également utilisée pour la complétion automatique dans l'éditeur généré par XText : ne seront suggérés que les identifiants correspondant à la règle. Il s'agit donc d'une extension proposée par XText pour prendre en compte une partie de la sémantique statique du langage.
En conséquence, les règles pour Initialization
et Action
correspondent à la même syntaxe, mais dans Action
, la valeur de l'attribut output
doit être un identifiant déjà rencontré dans le modèle.
Transformation Acceleo
[comment encoding = UTF-8 /] [module generate('http://www.xtext.org/example/mydsl/MyDsl')] [template private generateBool(b : Bool)] [if (b.isTrue)]true[else]false[/if] [/template] [template public generateElement(aModel : Model)] [comment @main/] [let className : String = aModel.name.toUpperFirst()] [file (className.concat('.java'), false, 'UTF-8')] import java.util.LinkedList; import java.util.List; public class [className/] implements Machine { private GUI myGUI; [for (init_out : Initialization | aModel.initializations)] private boolean [init_out.output.name/]_state = [generateBool(init_out.value)/]; [/for] @Override public void setGUI(GUI gui) { myGUI = gui; updateGUI(); } @Override public String['[]'/] getInputs() { String inputs['[]'/] = { [for (inp : Input | aModel.inputs) separator (',\n ')]"[inp.name/]"[/for] }; return inputs; } @Override public String['[]'/] getOutputs() { String outputs['[]'/] = { [for (out_init : Initialization | aModel.initializations) separator (',\n ')]"[out_init.output.name/]"[/for] }; return outputs; } @Override public void react(String input) { [for (rule : Rule | aModel.rules)] if ([if (rule.guard.output.oclIsUndefined())]true[else][if (rule.guard.isNeg)]![/if][rule.guard.output.name/]_state[/if]) { if (input.equals("[rule.trigger.name/]")) { [for (action : Action | rule.actions)] this.[action.output.name/]_state = [generateBool(action.value)/]; [/for] updateGUI(); return; } } [/for] } private void updateGUI() { List<String> activeOutputs = new LinkedList<String>(); [for (out_init : Initialization | aModel.initializations)] if (this.[out_init.output.name/]_state) { activeOutputs.add("[out_init.output.name/]"); } [/for] this.myGUI.setOutputs(activeOutputs); } public static void main(String['[]'/] args) { Machine m = new [className/](); new SwingGUI(m); } } [/file] [/let] [/template]
Commentaires sur la transformation Acceleo
On notera l'utilisation d'un [let ]
pour définir le nom de la classe générée, qui servira également à nommer le fichier produit.
Un template privé generateBool
a été défini pour générer la valeur true
ou false
en Java selon la valeur d'un booléen.
La seule difficulté dans cette transformation M2T est la gestion des règles sans garde.
Il aurait été possible de ne générer ni le if () {
ni l'accolade fermante correspondante lorsque la garde est vide, mais il est plus simple de générer un condition toujours vraie if (true) {
car cela évite une dépendance sur la même condition à deux endroits de la transformation. De plus, ce code sera optimisé par le compilateur Java, sa présence n'entraîne donc pas l'exécution de code inutile.
On remarque que la sémantique du langage est définie par cette transformation puisque c'est la première règle qui s'applique dont les actions sont exécutées, les autres étant ignorées. On voit ainsi que l'ordre dans lequel les règles sont déclarées définit le comportement du système, ce qui n'est a priori pas évident. Il aurait également été possible d'exécuter les actions de toutes les règles dont le déclenchement est possible.
Interpréteur en Xtend
Après avoir ajouté la classe MyDslLauncher
(qui permet de traiter les événements dans le menu Run As...
d'Eclipse), et la classe LaunchMyDslExecution
(qui récupère l'instance du modèle et lance l'exécution proprement dite) au package org.xtext.example.mydsl.ui.run
, il reste à écrire l'interpréteur de modèle.
Nous utilisons Xtend, qui a une syntaxe plus légère que Java, et qui permet d'ajouter des comportements à une classe sans en modifier le code source (d'où le nom Xtend
car cela permet d'extend
re des classes).
Les commentaires dans le code devraient vous permettre de comprendre comment cela fonctionne. Le principe est de créer une interface graphique pour le modèle à l'aide de Swing, puis de traiter les interactions entre l'utilisateur et cette interface pour faire réagir le système et mettre à jour l'interface graphique selon les sorties activées ou désactivées par la règle appliquée en réaction à un évènement.
Extension de la classe Model
package org.xtext.example.mydsl.interp import javax.swing.JFrame import java.awt.event.ActionListener import java.awt.event.ActionEvent import org.xtext.example.mydsl.myDsl.Model import java.awt.BorderLayout import javax.swing.JPanel import java.util.HashMap import org.xtext.example.mydsl.myDsl.Output import javax.swing.JCheckBox /* * Cette classe Xtend permet de définir une interface graphique pour la classe Model * du métamodèle généré par XText. * Elle hérite de JFrame, qui est la classe Swing (framework graphique Java) pour * afficher une fenêtre à l'écran. * Elle implémente l'interface ActionListener afin de pouvoir traiter les événements * de l'interface utilisateur. * C'est la méthode actionPerformed qui interprète le modèle en réponse aux actions de l'utilisateur. * * Elle utilise les class InputExt, OutputExt et RuleExt pour ajouter des méthodes * aux classes Input, Output, et Rule du metamodèle généré par XText */ class ModelExt extends JFrame implements ActionListener { extension InputExt inpext = new InputExt; extension OutputExt outext = new OutputExt; extension RuleExt rext = new RuleExt; val Model model val checkBoxes = new HashMap<Output, JCheckBox>() new(Model m) { super(m.name); defaultCloseOperation = JFrame.DISPOSE_ON_CLOSE model = m val inputPanel = new JPanel for (inp : m.inputs) { val btn = inp.makeInputButton btn.addActionListener(this) inputPanel.add(btn) } add(inputPanel, BorderLayout.NORTH) val outputPanel = new JPanel for (out : m.initializations) { val chk = out.makeCheckbox() checkBoxes.put(out.output, chk) outputPanel.add(chk) } add(outputPanel, BorderLayout.SOUTH) visible = true size = getPreferredSize } override actionPerformed(ActionEvent e) { var state = new HashMap<Output, Boolean> for (out : checkBoxes.keySet) { state.put(out, checkBoxes.get(out).selected) } if (model.rules.react(e.actionCommand, state)) { for (out : checkBoxes.keySet) { checkBoxes.get(out).selected = state.get(out) } } } }
Extension de la classe Input
package org.xtext.example.mydsl.interp import org.xtext.example.mydsl.myDsl.Input import javax.swing.JButton class InputExt { def makeInputButton(Input inp) { return new JButton(inp.name) } }
Extension de la classe Output
package org.xtext.example.mydsl.interp import org.xtext.example.mydsl.myDsl.Initialization import javax.swing.JCheckBox class OutputExt { def makeCheckbox(Initialization out) { val chk = new JCheckBox(out.output.name) chk.selected = out.value.isTrue chk.enabled = false return chk } }
Extension de la classe Rule
package org.xtext.example.mydsl.interp import org.eclipse.emf.common.util.EList import org.xtext.example.mydsl.myDsl.Rule import java.util.HashMap import org.xtext.example.mydsl.myDsl.Output class RuleExt { def dispatch boolean react(EList<Rule> rules, String evt, HashMap<Output, Boolean> state) { var newState = new HashMap<Output, Boolean> for (out : state.keySet){ newState.put(out, state.get(out)) } for (r : rules) { if (r.react(evt, newState)) { for (out : state.keySet){ state.put(out, newState.get(out)) } return true } } return false } def dispatch boolean react(Rule r, String evt, HashMap<Output, Boolean> newState) { if (r.isTriggered(evt, newState)) { for (action : r.actions) { newState.put(action.output, action.value.isTrue) } return true } return false } def isTriggered(Rule r, String evt, HashMap<Output, Boolean> state) { return ( (r.guard.output === null) || (state.get(r.guard.output) != r.guard.isIsNeg) ) && (r.trigger.name.equals(evt)) } }