Previous Up Next

Cours 2  Exemple de développement Gallina : Sémantique d’un mini-langage

Le but de ce cours est d’illustrer sur un exemple les fonctionnalités du langage de spécification Gallina associé au Calcul des Constructions Inductives et implanté dans l’assistant à la démonstration Coq.

L’exemple choisi traite de la sémantique d’un mini-langage de programmation impératif (typage, évaluation et sémantique axiomatique). Plusieurs solutions à la modélisation des différentes notions sont proposées. Les différentes constructions utilisées dans ce chapitre seront expliquées plus en détail dans les prochains cours.

2.1  Introduction

Gallina est le nom donné au langage de spécification de l’assistant Coq. Il permet de définir:

Ces caractéristiques du langage le rendent particulièrement adapté à la formalisation des mathématiques et notamment de l’informatique théorique.

On pourra notamment se faire une opinion sur ce slogan en consultant sur la page de Coq la liste des développements réalisés par les utilisateurs.
Les définitions intervenant en sémantique des langages se représentent particulièrement bien en Gallina:

Nous détaillons ici la formalisation de la sémantique d’un petit langage de programmation dans le style impératif.

Ce genre de formalisation (aussi appelé plongement profond) constitue une alternative à la preuve de programme impératif. Une autre alternative est le plongement simple (aussi appelé plongement superficiel) qui simule les propriétés des programmes impératifs à partir de leur interprétation dans un langage purement fonctionnel. La sémantique est alors implicite dans la traduction. Cette question sera évoquée dans le chapitre traitant de la preuve de programmes impératifs.

2.2  Présentation du problème

Notre langage comprend les commandes suivantes, à partir d’un ensemble de variables X :


C ::=
 skip
 |X:= E
 |C1;C2
 |if E then C1 else C2
 |while E do C
Figure 2.1: Syntaxe des commandes

Les expressions sont formées de constructions entières et booléennes simples et sont données par la syntaxe de la figure 2.2.


E ::=
 XsVariables sortées
 |true |  false Constantes booléennes
 |E1 xor E2Ou exclusif 
 |nConstantes entières
 |null ETeste si un entier est nul
 |E1 op E2Opération binaire sur les entiers
s ::=nat | bool 
op ::=+ | − | * | … 
Figure 2.2: Syntaxe des expressions

On cherche à définir des sémantiques statiques, opérationnelles naturelles et axiomatiques pour ce langage. Toutes sont représentées en sémantique naturelle par la définition d’une relation entre un programme et des “valeurs”. Cette relation sera décrite à l’aide des règles d’inférence.

2.2.1  Sémantique statique

Il s’agit de déterminer de manière statique sans l’exécuter qu’un programme est bien formé. Ici il faut vérifier que dans les expressions conditionnelles ou de boucle, les tests sont faits sur des expressions booléennes. Cela nous amène à définir une relation de typage sur les expressions. Les deux valeurs seront les deux sortes nat et bool.

La relation de typage E:s est définie par les axiomes et règles de la figure 2.3.


 Xs:s     true:bool     false:bool     n:nat
 
E1:bool   E2:bool
E1 xor E2:bool
         
E:nat
null E:bool
         
E1:nat   E2:nat
E1 op E2:nat
Figure 2.3: Sémantique statique des expressions

Pour les commandes, on définit la relation C: ok par les règles de la figure 2.4.


skipok       
E:s
X:= Eok
      
C1 ok  C2 ok
C1;C2ok
 
E:bool  C1ok  C2ok
if E then C1 else C2ok
         
E:bool  Cok
while E do C:ok
Figure 2.4: Sémantique statique des commandes

2.2.2  Sémantique opérationnelle

La sémantique opérationnelle définit le programme comme une transformation de l’état de la mémoire. Cette mémoire associe à chaque variable et sorte une valeur qui sera une constante entière n ou booléenne b. Les deux opérations utiles sur la mémoire sont la lecture et la mise à jour: si x est une variable et s une sorte alors m(x,s) représente la valeur de la mémoire pour la variable x et la sorte s, si v est une valeur alors m[xv] représente la mémoire où la variable x a été mise à jour par la valeur v. On a choisi de ne pas indiquer explicitement la sorte de la variable affectée, celle-ci sera déduite de la sorte de la valeur v.

Sémantique des expressions

On a besoin de la relation qui associe à chaque mémoire m et expression E une valeur v représentant le résultat de l’évaluation de E dans la mémoire m. On note cette relation mEv. On utilisera des constantes et des fonctions sur le domaine des valeurs réalisant les opérations booléennes et arithmétiques. Elles seront notées en italique en utilisant le même identificateur que dans la syntaxe: ainsi, à la construction syntaxique xor correspond la fonction sémantique xor .


 m ⊢ Xs▷ m(X,s)     m ⊢ true▷ true       m ⊢ false▷ false      m ⊢ n▷ n
 
m ⊢ E1▷ b1   m ⊢ E2▷ b2
m ⊢ E1 xor E2▷ b1 xor  b2
         
m ⊢ E▷ n
m ⊢ null E ▷ null  n 
         
m ⊢ E1▷ n1   m ⊢ E2▷ n2
m ⊢ E1 op E2▷ n1 op  n2
Figure 2.5: Sémantique des expressions

Sémantique des commandes

La relation mCm′ exprime que la commande C s’évalue dans une mémoire m qu’elle transforme en une mémoire m′. On la définit par les règles d’inférence de la figure 2.6.


m ⊢ skip ▷ m     
m ⊢ E ▷ v
m ⊢ X:=  E ▷ m[X← v]
       
m ⊢ C1 ▷ m1   m1 ⊢ C2 ▷ m
m ⊢ C1 ; C2 ▷ m
 
m ⊢ E ▷ true    m ⊢ C1▷ m
m ⊢ if E then C1 else C2 ▷ m
       
m ⊢ E ▷ false    m ⊢ C2▷ m
m ⊢ if E then C1 else C2 ▷ m
 
m ⊢ E ▷ true    m ⊢ C▷ m1    m1 ⊢ while E do C ▷ m′ 
m ⊢ while E do C ▷ m
       
m ⊢ E ▷ false 
m ⊢ while E do C ▷ m
Figure 2.6: Sémantique des commandes

2.2.3  Sémantique axiomatique

Il s’agit d’interpréter les commandes comme des transformations de prédicats, ces prédicats spécifiant des propriétés de la mémoire. Si P et Q sont deux tels prédicats et C une commande, alors on définit une relation {P} C {Q} dont l’interprétation est : l’exécution de C à partir d’une mémoire vérifiant P conduit à une mémoire vérifiant Q.

Nous aurons besoin de transformateurs de prédicats particuliers. La conjonction et l’implication de deux prédicats seront notés par PQ et PQ, si x est une variable et E une expression alors P[xE] représente le prédicat qui à toute mémoire m associe P(m[xv]) où v est la valeur telle que mEv. Si w est une valeur alors E=w représente le prédicat qui à tout m associe la propriété v=wv est la valeur telle que mEv.

La définition de cette relation est donnée par les règles d’inférence de la figure 2.7.


 {Pskip {P}     {P[X← E]} X :=  E {P}       
{PC1 {P1}   {P1C2 {Q}
{PC1 ; C2 {Q}
 
{P ∧ (E=true )} C1 {Q}      {P ∧ (E=false )} C2 {Q}
{Pif E then C1 else C2 {Q}
 
{P ∧ (E=true )} C {P}
{Pwhile E do C { P ∧ (E=false )}
 
P ⇒ P1    {P1C {Q1}   Q1⇒ Q
{PC {Q}
Figure 2.7: Sémantique axiomatique des commandes

On remarque que toutes ces définitions font intervenir des notions définies dans le méta-langage telles que la mémoire, les domaines sémantiques, les opérations sur ces domaines,…

2.2.4  Quelques propriétés

Parmi les propriétés intéressantes à montrer on a:

2.3  Spécification Gallina

Nous montrons comment représenter la théorie précédente en Gallina.

2.3.1  Les expressions

Définitions

On choisit de représenter les variables et les opérateurs binaires par des ensembles paramétriques qui pourront être instanciés dans un second temps. Par contre, les fonctions booléennes sont représentées de manière concrète par un constructeur du type de données. Ce choix permet d’illustrer deux traitements possibles des opérations du langage que l’on cherche à modéliser.

Coq < Parameter name : Set.

Coq < Inductive sort : Set :=
Coq < | Sbool : sort
Coq < | Snat : sort.

Coq < Parameter op : Set.

Coq < Inductive expr : Set :=
Coq < | Var : name -> sort -> expr
Coq < | Tr : expr
Coq < | Fa : expr
Coq < | Xor : expr -> expr -> expr
Coq < | Num : nat -> expr
Coq < | Null : expr -> expr
Coq < | Opn : op -> expr -> expr -> expr.

Expressions correctes

Le prédicat exprcorrect traduit la relation décrite dans la figure 2.3. Chaque règle d’inférence définissant la relation est traduite en un constructeur de la définition inductive. Les variables libres des définitions deviennent des variables quantifiées universellement dans les constructeurs.

Coq < Inductive exprcorrect : sort -> expr -> Prop :=
Coq < | corvar : forall (n:name) (s:sort), exprcorrect s (Var n s)
Coq < | cortr : exprcorrect Sbool Tr
Coq < | corfa : exprcorrect Sbool Fa
Coq < | corxor :
Coq <       forall b c:expr,
Coq <         exprcorrect Sbool b ->
Coq <         exprcorrect Sbool c -> exprcorrect Sbool (Xor b c)
Coq < | cornum : forall n:nat, exprcorrect Snat (Num n)
Coq < | cornull :
Coq <       forall e:expr, exprcorrect Snat e -> exprcorrect Sbool (Null e)
Coq < | corop :
Coq <       forall (o:op) (e f:expr),
Coq <         exprcorrect Snat e ->
Coq <         exprcorrect Snat f -> exprcorrect Snat (Opn o e f).

Domaines sémantiques

Il s’agit de représenter le domaine sémantique des variables, à chaque sorte de variable correspond un ensemble au sens mathématique qui sera représenté par un type de données Coq ici le type des booléens ou des entiers. Le domaine sémantique des valeurs semval est représenté par la somme disjointe des deux types.

Coq < Inductive semval : Set :=
Coq < | Bool : bool -> semval
Coq < | Nat : nat -> semval.

Interprétation des fonctions

Les fonctions sémantiques correspondant aux opérateurs concrets peuvent être explicitement programmées. Par contre la sémantique des opérations arithmétiques (qui sont vues de manière paramétrique) doit être fournie comme un paramètre.

Coq < Definition boolxor (b1 b2:bool) : bool :=
Coq <   if b1 then if b2 then false else true else b2.

Coq < Definition iszero (n:nat) : bool :=
Coq <   match n with
Coq <   | O => true
Coq <   | S n => false
Coq <   end.

Coq < Parameter semop : op -> nat -> nat -> nat.

Compatibilité entre sortes et valeurs

Il nous faudra relier la sorte des expressions et le type de leur interprétation sémantique. Pour cela, on définit sort_val une fonction des domaines sémantiques dans les sortes qui à chaque valeur sémantique associe la sorte correspondante.

Coq < Definition sort_val (v:semval) : sort :=
Coq <   match v with
Coq <   | Bool b => Sbool
Coq <   | Nat n => Snat
Coq <   end.

On utilisera également une relation compat entre les valeurs et les sortes telle que (compat Sbool  v) soit équivalent à ∃ b:bool.v=(Bool b) (défini par le prédicat valbool) et (compat Snat v) soit équivalent à ∃ n:nat.v=(Nat n) (défini par le prédicat valnat).

Coq < Inductive valbool : semval -> Prop :=
Coq <     valbool_intro : forall b:bool, valbool (Bool b).

Coq < Inductive valnat : semval -> Prop :=
Coq <     valnat_intro : forall n:nat, valnat (Nat n).

Coq < Definition compat (s:sort) (v:semval) : Prop :=
Coq <   match s with
Coq <   | Sbool => valbool v
Coq <   | Snat => valnat v
Coq <   end.

On remarque que le définition inductive de valbool est un codage inductif direct de la proposition fun x:semval => exists b:bool, x=Bool b. On peut aisément montrer la correspondance entre compat et sort_val.

Coq < Theorem compat_sort_val :
Coq <  forall (s:sort) (v:semval), compat s v -> s = sort_val v.

Une alternative est de représenter la notion de compatibilité par un prédicat inductif :

Coq < Inductive compat1 : sort -> semval -> Prop :=
Coq < | compat_bool : forall b:bool, compat1 Sbool (Bool b)
Coq < | compat_nat : forall n:nat, compat1 Snat (Nat n).

Les deux définitions sont équivalentes, remarquons que dans le second cas la propriété

(compat1 Snat  v) → ∃ n : natv=(Nat n)

nécessite une inversion du prédicat inductif alors que dans le cas de compat c’est une simple conséquence du calcul de l’expression Cases et de la définition de valnat.

La mémoire

La mémoire est représentée comme une fonction qui à toute variable et sorte associe une valeur sémantique, il faudra de plus supposer que cette valeur est compatible avec la sorte.

Coq < Definition memory := name -> sort -> semval.

Valeur d’une expression

Pour construire la relation entre une expression et sa valeur, on peut se donner une mémoire comme paramètre dans une “Section”, lorsque la section est fermée, les notions introduites seront automatiquement abstraites par rapport aux paramètres dont elles dépendent.

Coq < Section Valexpr.

Coq < Variable memstate : memory.

Coq < Hypothesis memstate_correct : 
Coq <     forall (n:name) (s:sort), compat s (memstate n s).

La définition exprval formalise la sémantique des expressions telle qu’elle est présentée à la figure 2.5.

Coq < Inductive exprval : expr -> semval -> Prop :=
Coq < | valvar : forall (n:name) (s:sort), exprval (Var n s) (memstate n s)
Coq < | valtr : exprval Tr (Bool true)
Coq < | valfa : exprval Fa (Bool false)
Coq < | valxor :
Coq <       forall (f g:expr) (bf bg:bool),
Coq <         exprval f (Bool bf) ->
Coq <         exprval g (Bool bg) -> exprval (Xor f g) (Bool (boolxor bf bg))
Coq < | valnum : forall n:nat, exprval (Num n) (Nat n)
Coq < | valtzero :
Coq <       forall (f:expr) (nf:nat),
Coq <         exprval f (Nat nf) -> exprval (Null f) (Bool (iszero nf))
Coq < | valopn :
Coq <       forall (o:op) (f g:expr) (nf ng:nat),
Coq <         exprval f (Nat nf) ->
Coq <         exprval g (Nat ng) -> exprval (Opn o f g) (Nat (semop o nf ng)).

On peut maintenant énoncer le théorème qui dit que toute expression correctement typée admet une valeur.

Coq < Theorem expr_val_cor : forall (E:expr) (s:sort),
Coq <    exprcorrect s E ->  exists v : semval, exprval E v.

La preuve par récurrence nécessite un lemme plus fort qui dit que la valeur calculée est compatible avec la sorte de l’expression.

Coq < Lemma expr_val_cor_dom : forall (E:expr) (s:sort),
Coq <    exprcorrect s E ->  exists2 v : semval, compat s v & exprval E v.

Représentation de la mémoire à l’aide de types dépendants

Le traitement de la relation entre la sorte de l’expression et le domaine de la valeur sémantique est lourd. On peut profiter de l’expressivité du langage de spécification de Coq pour utiliser d’autres formalisations. On construit une famille dépendante sval (dont le type est sortSet) qui à la sorte Sbool associe le type bool et à Snat associe le type nat. Le domaine sémantique semval pourrait être représenté par un couple formé d’une sorte s et d’un objet de type (sval s), ceci revient juste à coder une somme disjointe explicitement à l’aide d’un sélecteur s et d’un champ dont le type varie suivant le sélecteur.

On peut tirer partie de cette représentation pour simplifier la formalisation en rendant implicite dans la définition de la mémoire la notion de compatibilité.

Coq < Definition sval (s:sort) : Set :=
Coq <   match s with
Coq <   | Sbool => bool
Coq <   | Snat => nat
Coq <   end.

Coq < Definition memory1 := name -> forall s:sort, sval s.

On définit alors:

Coq < Parameter memstate1 : memory1.

Coq < Inductive exprval1 : expr -> forall s:sort, sval s -> Prop :=
Coq < | valvar1 :
Coq <       forall (n:name) (s:sort), exprval1 (Var n s) s (memstate1 n s)
Coq < | valtr1 : exprval1 Tr Sbool true
Coq < | valfa1 : exprval1 Fa Sbool false
Coq < | valxor1 :
Coq <       forall (f g:expr) (bf bg:bool),
Coq <         exprval1 f Sbool bf ->
Coq <         exprval1 g Sbool bg -> exprval1 (Xor f g) Sbool (boolxor bf bg)
Coq < | valnum1 : forall n:nat, exprval1 (Num n) Snat n
Coq < | valtzero1 :
Coq <       forall (f:expr) (nf:nat),
Coq <         exprval1 f Snat nf -> exprval1 (Null f) Sbool (iszero nf)
Coq < | valopn1 :
Coq <       forall (o:op) (f g:expr) (nf ng:nat),
Coq <         exprval1 f Snat nf ->
Coq <         exprval1 g Snat ng -> exprval1 (Opn o f g) Snat (semop o nf ng).

Le lemme de correction de l’évaluation s’énonce alors simplement :

Coq < Theorem expr_val_cor1 : forall (E:expr) (s:sort),
Coq <    exprcorrect s E ->  exists v : sval s, exprval1 E s v.

La formalisation et la preuve sont plus simples cependant l’usage de types dépendants rendra la formalisation de la fonction d’écriture sur la mémoire plus complexe en effet on doit avoir avec n:name,s:sort,v:(sval s),mem:memory1

Coq < Definition update : forall (m:nat) (s’:sort), sval s’.

Le simple fait de savoir que s=s′ ne suffit pas à assurer que le terme v de type (sval  s) est aussi de type (sval  s′). Il faudra utiliser une analyse par cas suivant les valeurs de s et s′. Une telle analyse n’aurait pas été possible si l’ensemble des sortes était resté paramétrique.

De manière générale, en présence de types dépendants, l’usage de l’égalité devient délicat. Il n’est pas possible par exemple d’écrire v=v′ avec v:(sval s) et v′:(sval s′) sauf lorsque s et s′ sont convertibles, la présence d’une hypothèse s=s′ est insuffisante. Il faudra utiliser des notions plus complexes d’égalité.

2.3.2  Vérification du type et évaluation constructive

On pourrait définir le fait qu’une expression e est mal formée comme la propriété :

∀ s:sort.¬ (exprcorrect s e)

En fait il est plus aisé de manipuler des définitions positives et donc de définir une notion constructive d’expression erronée en énumérant les cas d’échec possibles. Si on s’intéresse à l’interprétation constructive des preuves (que nous verrons plus tard) on remarque qu’une preuve que l’expression erronée permet de retrouver exactement la nature de l’erreur à savoir dans quel sous terme un opérateur a été appliqué à une expression d’une sorte non adaptée.

Coq < Inductive exprerr : expr -> Prop :=
Coq < | errxorl : forall b c:expr, exprcorrect Snat b -> exprerr (Xor b c)
Coq < | errxorr : forall b c:expr, exprcorrect Snat c -> exprerr (Xor b c)
Coq < | errtzero : forall b:expr, exprcorrect Sbool b -> exprerr (Null b)
Coq < | erropl :
Coq <       forall (op:op) (b c:expr),
Coq <         exprcorrect Sbool b -> exprerr (Opn op b c)
Coq < | erropr :
Coq <       forall (op:op) (b c:expr),
Coq <         exprcorrect Sbool c -> exprerr (Opn op b c)
Coq < | errcongxorl : forall b c:expr, exprerr b -> exprerr (Xor b c)
Coq < | errcongxorr : forall b c:expr, exprerr c -> exprerr (Xor b c)
Coq < | errcongtzero : forall b:expr, exprerr b -> exprerr (Null b)
Coq < | errcongopl :
Coq <       forall (op:op) (b c:expr), exprerr b -> exprerr (Opn op b c)
Coq < | errcongopr :
Coq <       forall (op:op) (b c:expr), exprerr c -> exprerr (Opn op b c).

Le théorème exprimant la décidabilité du typage et de l’évaluation est juste une preuve du fait que pour toute expression E, soit il est possible de construire une valeur v de l’expression dont la sorte est la sorte de l’expression soit il est possible de prouver que l’expression E est mal formée. Le fait d’utiliser une interprétation constructive de la disjonction et de l’existentielle assure l’existence d’un algorithme permettant de décider dans quel cas on est et de calculer effectivement la valeur. On établit un résultat de décidabilité sans avoir à représenter une notion de calculabilité.

Coq < Theorem expr_val_check_proof : forall E:expr,
Coq <    {v : semval | exprval E v &  exprcorrect (sort_val v) E} + {exprerr E}.

Si on ne s’intéresse qu’à la partie calcul de cette preuve alors on a une fonction eval_prog qui à toute expression associe une valeur ou rien du tout. Cette fonction peut également se représenter dans Gallina en utilisant le type option de Coq.

Coq < Print option.
Inductive option (A : Type) : Type :=
    Some : A -> option A | None : option A
For Some: Argument A is implicit
For None: Argument A is implicit and maximally inserted
For option: Argument scope is [type_scope]
For Some: Argument scopes are [type_scope _]
For None: Argument scope is [type_scope]

Coq < Implicit Arguments Some [A].

La fonction eval_prog se calcule par point fixe structurel sur l’expression.

Coq < Fixpoint eval_prog (e:expr) : option semval :=
Coq <   match e with
Coq <   | Var n s => Some (memstate n s)
Coq <   | Tr => Some (Bool true)
Coq <   | Fa => Some (Bool false)
Coq <   | Xor f g =>
Coq <       match eval_prog f, eval_prog g with
Coq <       | Some (Bool bf), Some (Bool bg) => Some (Bool (boolxor bf bg))
Coq <       | _, _ => None (A:=semval)
Coq <       end
Coq <   | Num n => Some (Nat n)
Coq <   | Null f =>
Coq <       match eval_prog f with
Coq <       | Some (Nat nf) => Some (Bool (iszero nf))
Coq <       | _ => None (A:=semval)
Coq <       end
Coq <   | Opn o f g =>
Coq <       match eval_prog f, eval_prog g with
Coq <       | Some (Nat nf), Some (Nat ng) => Some (Nat (semop o nf ng))
Coq <       | _, _ => None (A:=semval)
Coq <       end
Coq <   end.

La section Valexpr, ouverte page ?? est alors fermée ce qui a pour effet d’abstraire les définitions effectuées dans la section par rapport à memstate et à l’hypothèse memstate_correct lorsqu’elle est utilisée.

Coq < End Valexpr.

Coq < Check eval_prog.
eval_prog
     : memory -> expr -> option semval

2.3.3  Les commandes

La représentation de la syntaxe et de la sémantique statique des commandes suit les définitions des figures 2.1 et 2.4.

Syntaxe

Coq < Inductive com : Set :=
Coq <   | skip : com
Coq <   | aff : name -> expr -> com
Coq <   | seq : com -> com -> com
Coq <   | cond : expr -> com -> com -> com
Coq <   | while : expr -> com -> com.

Sémantique statique

Coq < Inductive comcorrect : com -> Prop :=
Coq < | corskip : comcorrect skip
Coq < | coraff :
Coq <       forall (n:name) (e:expr) (s:sort),
Coq <         exprcorrect s e -> comcorrect (aff n e)
Coq < | corseq :
Coq <       forall c d:com,
Coq <         comcorrect c -> comcorrect d -> comcorrect (seq c d)
Coq < | corcond :
Coq <       forall (b:expr) (c d:com),
Coq <         exprcorrect Sbool b ->
Coq <         comcorrect c -> comcorrect d -> comcorrect (cond b c d)
Coq < | corwhile :
Coq <       forall (b:expr) (c:com),
Coq <         exprcorrect Sbool b -> comcorrect c -> comcorrect (while b c).

2.3.4  Mise à jour de la mémoire

Nous définissons maintenant la fonction d’écriture dans la mémoire. Elle utilise la décidabilité de l’égalité sur les variables, qui vient de la décidabilité de l’égalité sur les noms (prise comme axiome puisque l’ensemble des noms n’est pas spécifié) et de celle sur les sortes qui peut explicitement être construite.
Pour montrer la décidabilité de l’égalité sur un ensemble A, on peut construire une fonction booléenne f de type AAbool et démontrer que c’est la fonction caractéristique de l’égalité (f x y)=truex=y. On choisit une autre solution qui est de construire un terme fdec de type ∀ x,y:A, {x=y}+{¬(x=y)} qui à tous x et y associe soit un objet (left h) avec h une preuve de x=y soit un objet (right h) avec h une preuve de ¬(x=y).
Une expression “if a=b then p else q” s’écrira dans Coq:

match (fdec a b) with left _ => p | right _ => q end.

Ou de manière équivalente :

ifdec (fdec a b) p q

On oublie l’information de preuve pour construire l’expression mais celle-ci pourra être facilement utilisée lorsqu’il s’agira de montrer des propriétés de cette expression.

Nous commençons par poser en axiome la décidabilité de l’égalité sur l’ensemble des noms et nous prouvons la décidabilité de l’égalité sur l’ensemble des sortes.

Coq < Parameter eq_name_dec : forall n m:name, {n = m} + {n <> m}.

Coq < Lemma eq_sort_dec : forall s s’:sort, {s = s’} + {s <> s’}.

Coq < decide equality.

Coq < Defined.

Nous pouvons définir maintenant l’opération update de mise à jour de la mémoire qui se fait dans une section introduisant le nom de la variable x, la valeur à affecter à cette variable v et la mémoire m. La sorte de la valeur est localement nommée s.

Coq < Section Update.

Coq < Variable x : name.

Coq < Variable v : semval.

Coq < Variable mem : memory.

Coq < Let s := sort_val v.

Coq < Definition update : memory :=
Coq <   fun (m:name) (s’:sort) =>
Coq <     ifdec (eq_sort_dec s s’) (ifdec (eq_name_dec x m) v (mem m s’))
Coq <       (mem m s’).

On montre ensuite les propriétés de base de cette fonction :

Coq < Theorem update_eq : v = update x s.

Coq < Theorem update_diff_name :
Coq <  forall (m:name) (s’:sort), x <> m -> mem m s’ = update m s’.

Coq < Theorem update_diff_sort :
Coq <  forall (m:name) (s’:sort), s <> s’ -> mem m s’ = update m s’.

Après avoir prouvé les lemmes, la section peut être refermée, les constructions sont alors paramétrées par x,v et m.

Coq < End Update.

2.3.5  Sémantique opérationnelle

La déclaration suivante implémente la relation décrivant la sémantique opérationnelle du langage de commande telle qu’elle est décrite dans la figure 2.6.

Coq < Inductive semcom : memory -> com -> memory -> Prop :=
Coq < | semskip : forall m:memory, semcom m skip m
Coq < | semaff :
Coq <       forall (m:memory) (n:name) (v:semval) (e:expr),
Coq <         exprval m e v -> semcom m (aff n e) (update n v m)
Coq < | semseq :
Coq <       forall (m m1 m’:memory) (c d:com),
Coq <         semcom m c m1 -> semcom m1 d m’ -> semcom m (seq c d) m’
Coq < | semcondtr :
Coq <       forall (m m’:memory) (e:expr) (c d:com),
Coq <         exprval m e (Bool true) ->
Coq <         semcom m c m’ -> semcom m (cond e c d) m’
Coq < | semcondfa :
Coq <       forall (m m’:memory) (e:expr) (c d:com),
Coq <         exprval m e (Bool false) ->
Coq <         semcom m d m’ -> semcom m (cond e c d) m’
Coq < | semwhiletr :
Coq <       forall (m m’:memory) (e:expr) (c:com),
Coq <         exprval m e (Bool true) ->
Coq <         forall m1:memory,
Coq <           semcom m c m1 ->
Coq <           semcom m1 (while e c) m’ -> semcom m (while e c) m’
Coq < | semwhilefa :
Coq <       forall (m:memory) (e:expr) (c:com),
Coq <         exprval m e (Bool false) -> semcom m (while e c) m.

2.3.6  Sémantique axiomatique

Nous nous intéressons finalement à la sémantique axiomatique. Une assertion est une propriété de la mémoire, représentée par un prédicat unaire. En logique de Hoare, ce prédicat unaire sera défini concrètement par une formule logique utilisant les variables du programme pour représenter les valeurs correspondantes de la mémoire.

Coq < Definition Assertion := memory -> Prop.

Transformations de prédicats

On étend les opérations usuelles de la logique à des transformations d’assertion:

Coq < Definition Istrue (E:expr) : Assertion :=
Coq <   fun m:memory => exprval m E (Bool true).

Coq < Definition Isfalse (E:expr) : Assertion :=
Coq <   fun m:memory => exprval m E (Bool false).

Coq < Inductive AndAss (P Q:Assertion) (m:memory) : Prop :=
Coq <     Conjass : P m -> Q m -> AndAss P Q m.

Coq < Definition ImplAss (P Q:Assertion) : Prop := forall m:memory, P m -> Q m.

Le transformateur suivant correspond à ce que nous avons noté P[XE]. Le terme (memupdate x E P) représente le prédicat qui est vrai en m si P(m[Xv]) est vérifié avec v la valeur de l’expression E dans la mémoire m.

Coq < Definition memupdate (x:name) (e:expr) (P:Assertion) : Assertion :=
Coq <   fun m:memory => forall v:semval, exprval m e v -> P (update x v m).

Définition de {P} c {Q}

On définit le prédicat (trueform P c Q) correspondant à {P} C {Q} telle qu’il est décrit dans la figure 2.7.

Coq < Inductive trueform : Assertion -> com -> Assertion -> Prop :=
Coq < | trueskip : forall P:Assertion, trueform P skip P
Coq < | trueaff :
Coq <       forall (P:Assertion) (n:name) (e:expr),
Coq <         trueform (memupdate n e P) (aff n e) P
Coq < | trueseq :
Coq <       forall (P Q R:Assertion) (c d:com),
Coq <         trueform P c R -> trueform R d Q -> trueform P (seq c d) Q
Coq < | truecond :
Coq <       forall (P Q:Assertion) (e:expr) (c d:com),
Coq <         trueform (AndAss P (Istrue e)) c Q ->
Coq <         trueform (AndAss P (Isfalse e)) d Q -> trueform P (cond e c d) Q
Coq < | truewhile :
Coq <       forall (P:Assertion) (e:expr) (c:com),
Coq <         trueform (AndAss P (Istrue e)) c P ->
Coq <         trueform P (while e c) (AndAss P (Isfalse e))
Coq < | truecons :
Coq <       forall (P P1 Q Q1:Assertion) (c:com),
Coq <         ImplAss P P1 ->
Coq <         trueform P1 c Q1 -> ImplAss Q1 Q -> trueform P c Q.

Lemme de correction

Le théorème suivant énonce la correction de la relation donnée {P} c {Q} par rapport à la sémantique.

Coq < Theorem truecorrect : forall (P Q:Assertion) (c:com),
Coq <    trueform P c Q -> forall m1 m2:memory, semcom m1 c m2 -> P m1 -> Q m2.

2.4  Pour en savoir plus

2.4.1  Sémantique des langages et compilateurs

Les logiques d’ordre supérieur comme le Calcul des Constructions Inductives se prêtent bien aux formalisations de notions sémantiques et logiques. Une des premières preuves de cette nature a été effectuée par Samuel Boutin. Il s’agissait du schéma de compilation d’un mini-ml vers la CAM (Categorical Abstract Machine) tel qu’il était décrit dans l’article [CDDK86]. Yves Bertot a étudié la preuve du compilateur d’un langage impératif vers un langage assembleur [Ber98].

Des preuves de compilateurs plus conséquents ont ensuite été entreprises: Delphine Terrasse [Ter92] a ébauché la preuve d’un compilateur Esterel, Pablo Argon [Arg98] a extrait le noyau du compilateur du langage Electre exprimé comme l’exécution des règles de la sémantique, des équipes de recherche de Dassault et Aérospatiale ont étudié la formalisation d’un compilateur pour le langage Lustre tel qu’il est implanté dans l’outil Scade, une partie de ces développements est disponible domme contribution au système Coq. On trouvera également dans les contributions des formalisations de plusieurs calculs de processus, en particulier le π-calcul ainsi que des modélisations de logique temporelle.

2.4.2  Logique de Hoare

La formalisation de la logique de Hoare a été effectuée par Thomas Kleymann-Schreiber [Sch97, Kle98] dans l’assistant LEGO dont le langage s’apparente au Calcul des Constructions Inductives. Une formalisation dans l’outil HOL a également été réalisée par Peter Homeier, l’objectif principal de ces développements est de justifier les propriétés fondamentales de la sémantique axiomatique comme la correction et la complétude, ce qui n’est pas évident dès que le langage comporte des constructions avancées comme des appels de procédure.

2.4.3  Preuve de programmes Java

Un domaine actif de recherche est actuellement l’étude des propriétés de sécurité des programmes Java, qu’ils soient utilisés sur l’internet ou les cartes à puce. Pour garantir de telles propriétés, il est essentiel d’avoir une description précise de la sémantique de ce langage, au niveau du code source ou du byte-code, que ce soit la sémantique statique, dynamique ou axiomatique. Le projet Bali #1http://isabelle.in.tum.de/Bali à l’université de Munich formalise ces notions dans l’outil Isabelle/HOL. Des développements analogues sont réalisés à l’aide de Coq en France, dans le projet Lemme à l’INRIA Sophia-Antipolis, dans le projet Lande à l’IRISA ou par la société Trusted Logic. Les définitions inductives sont utilisées de manière intensive dans ces développements.

2.4.4  Plongement superficiel ou profond

Lorsqu’on veut étudier les programmes d’un langage X, on peut procéder de deux manières. La première, appelée plongement profond (deep embedding en anglais), consiste à introduire un type concret dans Coq pour représenter les arbres de syntaxe abstraite du langage X. La seconde appelée plongement superficiel (shallow embedding en anglais), consiste à représenter directement un programme de X par sa sémantique exprimée dans le langage mathématique du système Coq.

Plongement profond

Dans le plongement profond, on dispose d’un type concret Y qui représente les arbres de syntaxe abstraite du langage X. On peut ensuite construire des fonctions et des relations sur ce type. On pourra ainsi parler des expressions bien formées, construire des algorithmes de typage, représenter la sémantique du langage. Un programme écrit dans le langage X pourra être représenté par un objet Coq de type Y, les propriétés de cet objet seront établies en utilisant différentes sémantiques.

Dans notre exemple, les programmes sont représentés par un plongement profond.

Plongement superficiel

Dans le plongement superficiel, un programme est directement traduit en sa sémantique. Par exemple, on pourrait commencer par introduire une notion de mémoire et identifier un programme à une fonction de transformation des mémoires. La séquence de deux programmes pourrait être définie comme la composition des mémoires les représentant. Les relations sur les programmes comme les sémantiques opérationnelle ou axiomatique peuvent être définies en faisant référence à la sémantique des programmes. Les règles d’inférence deviennent alors des théorèmes qui peuvent être utilisés pour prouver des propriétés de programmes. La représentation des propriétés dans la sémantique axiomatique utilise un plongement superficiel (on n’introduit pas la syntaxe des formules).

Cette représentation peut permettre de raisonner rapidement sur les propriétés de programmes particuliers. Par contre, elle ne permet pas de construire des programmes ou de faire des preuves par récurrence sur la structure des programmes. Elle n’est donc pas adaptée à l’étude méta-théorique des propriétés du langage.


Previous Up Next