Programmation synchrone 2022/2023 – Journal du cours

Table des matières

Ce fichier est disponible au format HTML.

1. Cours 1 <2022-09-27 mar.>

On lit le syllabus, les notes jusqu'à la page 10.

Le site d'Heptagon n'est plus disponible, mais le manuel peut être trouvé sur le dépôt du compilateur. On peut cliquer sur le bouton download pour télécharger une copie PDF.

https://gitlab.inria.fr/synchrone/heptagon/-/blob/ec26be27b91f3e601b98b8b7e15e8d56d4b9afc7/manual/heptagon-manual.pdf

2. Cours 2 <2022-10-04 mar.>

On lit les notes jusqu'à la page 16.

3. Cours 3 <2022-10-11 mar.>

On lit les notes jusqu'à la page 23.

4. Cours 4 <2022-10-18 mar.>

On lit les notes jusqu'à la page 31.

5. Cours 5 <2022-10-25 mar.>

On lit les notes jusqu'à la page 38.

6. Cours 6 <2022-11-08 mar.>

On discute du projet. Le code et le sujet peuvent être trouvés dans le répertoire projet.

Pour réaliser le projet, il faut un compte sur Gaufre, le serveur GitLab de l'UFR d'informatique.

https://gaufre.informatique.univ-paris-diderot.fr/

Si vous avez déjà un compte à l'UFR d'informatique, celui-ci permet de vous connecter à Gaufre. C'est notamment le cas de tous les étudiants de M2. Pour les étudiants de l'EIDD, je suis en train de faire le nécessaire ; je vous tiens au courant d'ici à la semaine prochaine.

7. Cours 7 <2022-11-15 mar.>

On traite de l'exemple du sous-répertoire audio.

8. Cours 8 <2022-11-22 mar.>

8.1. Pendule inversé

On traite de l'exemple du sous-répertoire inverted-pendulum.

8.2. Introduction à la vérification de modèle avec Kind2

L'outil Kind2 est un vérificateur de modèle (model checker) pour un langage proche d'Heptagon développé à l'université de l'Iowa.

https://kind.cs.uiowa.edu

Un tel outil est une des justifications pour l'emploi d'un langage très limité comme Heptagon, SCADE et leurs variantes. La vérification automatique est beaucoup plus difficile lorsque le langage d'entrée est Python.

8.2.1. Introduction

Le langage accepté par Kind2 est une variante d'Heptagon beaucoup plus proche de son ancêtre Lustre. Les différences sont assez mineures mais peuvent rendre l'écriture du code désagréable lorsqu'on est habitué aux constructions d'Heptagon.

Par exemple, le langage ne propose pas l'opérateur fby, uniquement pre et ->. Notre classique compteur doit donc être écrit comme suit.

node counter() returns (o : int)
let
  o = 0 -> (pre o + 1);
tel

Kind2 est capable de traduire ce programme vers du code impératif, jouant ainsi le même rôle que le compilateur heptc pour Heptagon. Son intérêt est néanmoins ailleurs : il est capable de vérifier des propriétés.

Certains outils demandent d'utiliser un langage spécialisé pour exprimer les propriétés, par exemple une logique temporelle pour le model-checker PRISM. Les outils de vérification de langages synchrones comme Kind2 font un choix un peu différent. Le langage des propriétés est le même que celui des programmes, et la propriété à vérifier est exprimée par un flot booléen désigné par l'utilisateur doit toujours être vrai. Avec cette approche, on va avoir tendance à écrire des observateurs synchrones, c'est-à-dire des noeuds qui sont utilisés uniquement pour vérifier que le noeud qui va être déployé sur le système embarqué final va respecter certaines propriétés, éventuellement sous certaines hypothèses concernant ses entrées.

node top() returns (ok : bool)
var o : int;
let
  o = counter();
  ok = o >= 0;
  --%PROPERTY ok;
tel

En lançant Kind2 sur ce fichier, on lui demande de vérifier que le flot ok est toujours vrai. Il est capable de vérifier que c'est le cas, même si ok contient une infinité de valeur ; intuitivement, la décidabilité du problème est conséquence du caractère fini de la mémoire de tout programme synchrone.

$ kind2 ex0.lus
 kind2 v1.6.0

====================================================================================================================
Analyzing top
  with First top: 'top'
	     subsystems
	       | concrete: counter

<Success> Property ok is valid by property directed reachability after 0.041s.

--------------------------------------------------------------------------------------------------------------------
Summary of properties:
--------------------------------------------------------------------------------------------------------------------
ok: valid (at 1)
====================================================================================================================

(Cet exemple montre que Kind2 suppose que la sémantique du type int de Lustre est celle des entiers relatifs, sans limite de taille. Pourquoi ?)

On peut montrer des propriétés un peu plus intéressantes, par exemple en écrivant une variante du compteur avec une valeur d'initialisation.

node counterini(ini : int) returns (o : int)
let
  o = ini -> (pre o + 1);
tel

node top(ini : int) returns (ok : bool)
var o : int;
let
  o = counterini(ini);
  ok = o >= 0;
  --%PROPERTY ok;
tel

Cet exemple est plus intéressant que le précédent en ce que le noeud top dispose désormais d'une entrée. Kind2 va donc essayer de vérifier que toutes les valeurs de la suite ok sont vraies et ce pour toutes les suites d'entrée ini. Va-t-il réussir ?

$ kind2 ex0.lus
 kind2 v1.6.0

====================================================================================================================
Analyzing top
  with First top: 'top'
             subsystems
               | concrete: counterini

<Failure> Property ok is invalid by bounded model checking for k=0 after 0.041s.

Counterexample:
  Node top ()
    == Inputs ==
    ini    -1
    == Outputs ==
    ok  false
    == Locals ==
    o      -1

  Node counterini (top[l14c7])
    == Inputs ==
    ini -1
    == Outputs ==
    o   -1


--------------------------------------------------------------------------------------------------------------------
Summary of properties:
--------------------------------------------------------------------------------------------------------------------
ok: invalid after 0 steps
====================================================================================================================

La vérification a échoué, mais Kind2 nous a fourni un contre-exemple sous la forme d'une séquence d'entrée qui falsifie la propriété. Il suffit bien sûr de donner un ini strictement négatif au premier instant !

Notre propriété est donc fausse. Peut-on exprimer que le flot o est toujours vrai mais à condition que la première valeur de ini soit positive ? Oui, en utilisant l'opérateur booléen 'implication pour exprimer une précondition (hypothèse) sur l'entrée ini.

(Remarque : l'implication dont il s'agit ici n'est que l'opérateur combinatoire sur les flots défini par x => y = not x or y, rien de plus. En particulier, Kind2 n'a aucun traitement spécifique de cet opérateur.)

Un premier essai pourrait ressembler à ce qui suit.

node top(ini : int) returns (ok : bool)
var o : int;
let
  o = counterini(ini);
  ok = ((ini >= 0) -> true) => (o >= 0);
  --%PROPERTY ok;
tel

La vérification échoue encore, avec le contre-exemple qui suit.

Counterexample: Node top () = Inputs = ini -2 0 = Outputs = ok true false = Locals = o -2 -1

Node counterini (top[l19c7]) = Inputs = ini -2 0 = Outputs = o -2 -1

Comment comprendre ce contre-exemple ? Au premier instant, i vaut -2 et la précondition sur ini est fausse, donc l'implication est vraie. Au deuxième instant, la précondition sur ini est vraie, donc l'implication prend la valeur de o >= 0, qui se trouve fausse.

Quel est le problème ? Ce que nous avions en tête n'est pas ce que nous avons écrit. On aurait voulu dire "si ini < 0 au premier instant, la précondition est fausse pour toujours", mais on a écrit "si ini < 0 au premier instant, la précondition est fausse au premier instant". On peut régler le problème en introduisant un noeud always dont la sortie devient fausse pour toujours dès que son entrée est fausse.

node always(b : bool) returns (o : bool)
let
  o = (true -> pre o) and b;
tel

node top(ini : int) returns (ok : bool)
var o : int;
let
  o = counterini(ini);
  ok = always((ini >= 0) -> true) => (o >= 0);
  --%PROPERTY ok;
tel

La vérification réussie. En particulier, le contre-exemple précédent n'est plus valide. Si la valeur initiale de ini est -2, la valeur de always((ini >= 0) -> true) sera toujours fausse, et donc l'implication toujours vraie.

Le fait d'exprimer une précondition sur les entrées en utilisant des noeuds comme always est suffisamment fréquent pour que Kind2 propose un support dédié.

8.2.2. Comparaison de noeuds

On peut utiliser le principe des observateurs synchrones pour comparer le comportement de deux noeuds, et éventuellement tester leur équivalence.

Par exemple, comment quelle relation voyez vous entre notre premier noeud counter et le noeud counterini ? Et comment la feriez-vous vérifier par Kind2 ?

Une solution est le noeud suivant, qui vérifie qu'ajouter la première valeur de ini au noeud counter() donne le même résultat que inicounter(ini).

node compare(ini : int) returns (ok : bool)
var ini0 : int;
let
  ini0 = ini -> pre ini0;
  ok = counter() + ini0 = counterini(ini);
  --%PROPERTY ok;
tel

8.2.3. Comment ça fonctionne, dans les grandes lignes ?

On se ramène à un problème logique en traduisant un noeud Lustre en un système de transition à état fini. Il s'agit d'un système décrit par des formules de logique propositionnelle, et les algorithmes de vérification utilisent donc des solveurs SAT et SMT.

Rappels de logique élémentaire : une valuation \(s\) est une fonction des variables propositionnelles dans les booléens. Une valuation \(s\) satisfait une formule \(F\), ce qu'on note \(s \models F\), lorsque la formule obtenue en remplaçant toute variable \(x\) dans \(F\) par \(s(x)\) est vraie. On écrit \(F \Rightarrow G\) lorsque toute valuation satisfaite par \(F\) est satisfaite par \(G\).

Dans ce qui suit, on suppose que pour chaque variable propositionnelle \(x\) on dispose d'une variable propositionnelle choisie \(x'\) distincte de tous les autres \(y\) et \(y'\). Si \(F\) est une formule, \(F'\) désigne la même formule où toute variable \(x\) a été substituée par \(x'\).

Un système de transition à état fini est un triplet \(S = (\overline{x}, I, T)\) où \(\overline{x}\) est une liste de variables propositionnelles appelée variables d'état, \(I\) une formule logique dont les variables libres appartiennent à \(\overline{x}\) appelée états initiaux, et \(T\) une formule logique sur les variables \(\overline{x}\) et les variables \(\overline{x}'\) appelée relation de transition.

Un état \(s\) est une valuation de \(\overline{x}\).

Une trajectoire est une suite d'états \(s_0,s_1,\dotsc\) telle que \(s_0 \models I\) satisfait \(I\) et \(s_i, s_{i+1}' \models T\). Autrement dit, c'est une suite d'états telle que le premier est initial et chaque paire d'états consécutifs de la suite appartient à la relation de transition.

Un état qui apparaît dans une trajecotire est dit atteignable.

Le problème de vérification de modèle correspond à la donnée d'une propriété \(P\), définie comme un prédicat sur \(\overline{x}\), et donc un ensemble d'états. Le but d'un vérificateur va être de prouver que tout état \(s\) atteignable est tel que \(s \models P\).

(Dans le cas de Kind2, les propriétés \(P\) vont décrire exactement l'ensemble des états où ok vaut true.)

La stratégie générale pour démontrer que \(P\) est un invariant est toujours la même : on cherche une formule \(F\) qui satisfait les trois propriétés suivantes.

  1. \(I \Rightarrow F\)
  2. \(F \wedge T \Rightarrow F'\)
  3. \(F \Rightarrow P\).

On dit qu'une formule \(F\) qui satisfait les deux premières propriétés est inductive.

  1. IC3/PDR

    La méthode dite d'atteignabilité guidée par la propriété (property-directed reachability ou PDR, aussi appelée IC3) est relativement récente (2011) et constitue l'état de l'art dans le domaine. C'est celle utilisée par Kind2 pour les problèmes qu'il estime difficile. Sa description dépasse le cadre de cette séance introductive, et on va plutôt se concentrer sur une méthode plus simple, la $k$-induction.

  2. La $k$-induction

    La méthode de preuve par $k$-induction consiste à dérouler.

    1. La 1-induction

      On demande au solveur SAT de démontrer que \(P\) est inductive, i.e.,

      1. \(I \Rightarrow P\),
      2. \(P \wedge T \Rightarrow P'\).

      Si cela fonctionne, on a gagné. Sinon, on essaie la 2-induction.

    2. La 2-induction

      On demande au solveur SAT de démontrer que \(P\) satisfait :

      1. \(I \Rightarrow P\),
      2. \(I \wedge T \Rightarrow P'\),
      3. \(P \wedge T \wedge P' \wedge T' \Rightarrow P''\).

      Si cela fonctionne, on a gagné. Sinon, on essaie la 3-induction.

    3. La 3-induction

      On demande au solveur SAT de démontrer que \(P\) satisfait :

      1. \(I \Rightarrow P\),
      2. \(I \wedge T \Rightarrow P'\),
      3. \(I \wedge T \wedge T' \Rightarrow P''\),
      4. \(P \wedge T \wedge P' \wedge T' \wedge P'' \wedge T'' \Rightarrow P'''\).

      Si cela fonctionne, on a gagné. Sinon, on essaie la…

    4. Le \(k\) général

      On écrit \(F'^i\) pour \(F\) avec \(i\) primes appliqués.

      On demande au solveur SAT de démontrer que \(P\) satisfait :

      1. \(\bigwedge_{i = 0}^k (I \wedge (\bigwedge_{j = 0}^{i-1} T'^j)) \Rightarrow P'^i)\)
      2. \((\bigwedge_{i = 0}^{k-1} (P'^i \wedge T'^i)) \Rightarrow P'^k\).

      Autrement dit, que si on montre que \(P\) est satisfait par tous les états atteignables en \(k-1\) transitions, et que \(P\) est préservé par toutes les transitions venant après \(k-1\) transitions dont les états satisfaisaient \(P\), alors \(P\) est inductive.

Auteur: Adrien Guatto

Created: 2022-11-22 mar. 14:05

Validate