• Aucun résultat trouvé

2.4 Langages synchrones et model checking

2.4.2 Le langage Lustre

Création et utilisation

Le langage Lustre a été créé en 1984 par Nicolas Halbwachs et Paul Caspi dans le cadre d'un projet de recherche au laboratoire Verimag de Grenoble. Une dizaine d'années plus tard, Lustre a été industrialisé par la société Verilog, au c÷ur de l'environnement de développe- ment SCADE (Safety Critical Application Development Environment). Cet outil est utilisé pour la conception de systèmes critiques, en particulier dans le domaine de l'embarqué, notamment par l'avionique (Airbus, Eurocopter).

Principes de base

L'une des spécicités de Lustre est d'être un langage de ots. Ceci signie que toute variable est un ot de données, c'est-à-dire un couple constitué d'une horloge et d'une suite innie de valeurs typées. Ainsi, une variable ne porte pas une valeur unique, mais une valeur à chaque instant logique : dire que la variable x vaut a revient donc à dire qu'à chaque instant i (où xi est déni), xi vaut ai.

Lustre est un langage déclaratif : un programme Lustre est constitué d'un ensemble de dé- clarations dénissant les relations qui existent entre les diérents ots. En programmation déclarative, on décrit plutôt le problème (le quoi) que l'algorithme explicite pour le résoudre (le comment). Lustre privilégie une vision fonctionnelle de la programmation déclarative : les relations entre les ots sont alors décrites par des équations mathématiques vraies pour tous les instants. Alors que dans de nombreux langages de programmation le symbole = re- présente l'aectation, en Lustre il représente l'égalité mathématique. Le compilateur Lustre n'autorisera donc pas la déclaration x = x+1 : cette dénition cyclique est contradictoire, car à un instant donné, la variable ne peut pas posséder à la fois une valeur et la même valeur incrémentée de 1.

Introduction au langage En Lustre, on retrouve :

• les types de base bool, int, real,

• les opérateurs arithmétiques +, -, *,/,mod, • les opérateurs logiques or, and, not,

• les opérateurs de comparaison =, <>, <, >, >=, <=.

On trouve également l'opérateur de contrôle if then else.

Ces opérateurs classiques sont à remettre dans le contexte des ots : il faut considérer les opérations  point à point , pour chaque instant logique.

Exemple: Étant donnés deux ots x et y, le ot x + y vaut pour tout instant i (où xi et yi

sont dénis), xi+ yi soit la valeur de x à l'instant i additionnée à la valeur de y au même

instant.

De la même manière, l'opérateur if then else qui est généralement traduit par  si... alors... sinon  devrait plutôt est interprété comme  quand... alors... sinon . Prenons l'exemple de la déclaration suivante : x = if y then 2 else 5 où x est de type int et y de type bool. La valeur de x vaut 2 quand y est vrai et 5 sinon. La valeur du ot x est ainsi calculée en évaluant l'équation à chaque temps logique. En électronique, cet opérateur est appelé multiplexeur (Figure 2.15).

Figure 2.15  Schéma logique d'un multiplexeur. Lustre permet également une représentation gra- phique de ce type. Cette représentation, proposée notamment dans l'environnement de développement SCADE, est particulièrement adaptée au domaine de l'électronique.

Le langage Lustre est minimaliste, mais il peut facilement être enrichi par des opérateurs construits. Pour satisfaire l'hypothèse de programmation synchrone, le langage Lustre n'au- torise aucune structure qui pourrait rendre le temps de calcul non borné. En particulier, il n'y a ni boucles ni fonctions récursives. Par contre, il existe en Lustre une notion de récur- sivité temporelle qui peut être facilement employée grâce aux opérateurs temporels pre() et ->. L'opérateur pre() permet d'accéder à la valeur d'un ot à l'instant précédent où il était déni, tandis que l'opérateur -> ( suivi de ) concerne la valeur à tous les instants suivant le premier.

Exemple: La déclaration x = 0 -> pre(x) + 1 initialise le ot x à 0 et les valeurs sui- vantes sont dénies par un principe de récursion temporelle : aux temps suivants, x vaudra 0 + 1 = 1 puis 1 + 1 = 2 puis 2 + 1 = 3 et ainsi de suite. Cette déclaration dénit donc récursivement le ot x comme la suite des entiers naturels.

Il existe d'autres opérateurs que l'on ne détaillera pas ici, car notre objectif n'est pas de donner une description exhaustive du langage, mais simplement d'introduire les éléments permettant d'appréhender le Chapitre 8. De plus, dans notre cas d'utilisation (application du model checking), seul un sous-ensemble du langage Lustre peut-être pris en compte, rendant certains types et opérateurs inutilisables (par exemple les tableaux).

Un programme Lustre est structuré en n÷uds, ce qui le rend particulièrement modulaire. L'en-tête d'un n÷ud commence par le mot clé node, suivi du nom du n÷ud et contient la déclaration typée des ots d'entrée et des ots de sortie. Des variables locales peuvent ensuite être déclarées à la suite du mot clé var. Le corps du n÷ud, délimité par les mots clés let et tel, est un ensemble d'équations (non ordonnées) qui dénissent les ots de sorties en fonction des ots d'entrée à chaque instant. Un n÷ud a donc la structure suivante :

node nomDuNoeud(Entrée1: typeE1; ...) returns(Sortie1: typeS1; ...); var localVar1: typeV1; ...;

let

Équations; tel

Le n÷ud principal, qui détermine les entrées et sorties du système, est choisi à la compilation.

Quelques mots sur la compilation

Classiquement, lorsque l'on compile un n÷ud Lustre, il se produit tout d'abord une vérica- tion statique. Ces vérications concernent notamment l'absence d'appels récursifs de n÷uds, l'absence de déclarations non initialisées et l'absence de dénitions cycliques.

La compilation permet de générer du code séquentiel. Il s'agit d'une boucle innie qui repré- sente l'écoulement du temps logique et qui contient le code correspondant à une itération du n÷ud principal. Ce code permet donc de lire les entrées, de calculer les sorties et de les mémoriser les valeurs utiles aux instants suivants.

Il existe deux chemins dans la compilation. Le premier consiste à générer du code dc par l'outil lus2dc. Ce format contient un langage déclaratif qui peut également servir d'intermé- diaire à d'autres langages synchrones (Esterel et Signal). À partir du code dc, l'outil drac génère du code C. L'autre possibilité, la plus courante, est de générer du code ec (expended code) grâce à l'outil lus2ec. Il s'agit en fait d'un code Lustre constitué d'un seul n÷ud par expansion des appels aux autres n÷uds. C'est ensuite l'outil ec2c qui se charge de produire du code C. Le format ec est le format qui sert de base aux outils de vérications de Lustre. Rappelons que l'ordre des équations n'a pas d'importance en Lustre. Or dans le code sé- quentiel généré à la compilation, l'ordre des actions correspondantes est important. Pour ordonner ces actions, un tri topologique est fait sur le graphe de dépendance des va- riables [Halbwachs, 1993] [Collet et al., 2001].

Une fois compilé, le code Lustre peut être exploité de diérentes manières. En particulier, l'outil Luciole permet de simuler des n÷uds Lustre, et les diérents ots (entrées/sorties) peuvent être visualisés sous forme de chronogramme par Sim2chro (Figure 2.16).

(a) Fenêtre de simulation avec Luciole.

(b) Chronogramme avec l'outil Sim2chro.

Figure 2.16  Fonctionnement de Luciole pour l'exemple simple d'un ot booléen y déni par y = false -> pre(x) où x est également un ot booléen : y est initialisé à faux puis il prend la valeur de x à l'instant précédent. (a) Le bouton step de la fenêtre de simulation permet de passer à l'instant suivant et le bouton x permet de donner à x la valeur vraie à l'instant courant. Lorsque y est vrai, la zone grisée devient rouge. (b) Le chronogramme permet de visualiser les ots x et y.

La sous-section suivante décrit une autre possibilité en Lustre : la vérication de propriétés par model checking.