CentraleSupélecDépartement informatique
Plateau de Moulon
3 rue Joliot-Curie
F-91192 Gif-sur-Yvette cedex
Solution des exercices du bureau d'étude du 9 janvier 2024

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'attribut name d'un Model.
  • += 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'attribut inputs d'un Model sera une séquence d'objets de type Input
  • ?= donne la valeur true à 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 dans Guard, et pour détecter la reconnaissance du mot-clef true dans MyBoolean.

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'extendre 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))
	}
}