Langages de programmation
Sémantiques opérationnelles
Interpréteurs
Le langage Pseudo-Pascal.

Postscript, Luc Maranget Le poly

La chaîne de compilation



Langages de programmation


Langages généraux Ils doivent être complets, ie. permettre d'exprimer tous les algorithmes calculables. Au minimum, il faut une mémoire infinie (grande en pratique) et la possibilité d'exprimer la récursion (construction primitive ou boucle while). Ex: Fortran, Pascal, C, Ocaml, etc.


Langages spécialisés (DSL pour Domain Specific Languages) Langages pour le graphisme, pour commander des robots, la calculette. Ils peuvent ne pas être complets.


Expressivité Les langages généraux ne sont pas tous équivalents. L'expressivité est la capacité d'exprimer des algorithmes succinctement (et directement).

Exemples de constructions expressives


Les structures de données (et filtrage)


Les modules, les objets


Le typage
Restreint l'expressivité au profit de la sécurité. (Comparer Scheme et ML).

L'expressivité du système de typage est aussi importante (comparer ML et Pascal ou Java).


Les fonctions

Syntaxe concrète -- syntaxe abstraite


La syntaxe concrète Support concret du discours : une suite de caractères. Une grammaire dit comment les lettres forment des mots et des phrases.
La syntaxe abstraite est une représentation arborescente qui fait abstraction de la notation (on parle d'arbre de syntaxe abstraite). Elle définit les constructions du langage.


L'analyse lexicale traduit une suite de caractères en suite de mots. L'analyse grammaticale lit une suite de mots, reconnaît les phrases du langages et retourne un arbre de syntaxe abstraite.

Exemple simple: La calculette


Syntaxe concrète (dans le style BNF)
    expression ::= ENTIER
                 | IDENTIFICATEUR
                 | expression binop expression
                 | "(" expression ")"
    binop      ::= "+" | "-" | "*" | "/"



Syntaxe abstraite
    type expression =
      | Const 
of int
      | Variable 
of string
      | Bin 
of binop * expression * expression
    and
 binop = Plus | Moins | Mult | Div



Exemple les expressions « (1 - x) * 3 » et « (1-x)*(3) » ont pour représentation
   Bin (Mult,
     Bin (Moins, Const 1, Variable "x"), Const 3)
Ou plutôt :
Essentiellement : les parenthèses disparaissent.

Sémantique des programmes

Il s'agit de donner un sens aux programmes.


Sémantique informelle L'instruction while est de la forme :
while (expression) instruction
la sous-instruction est exécutée de manière répété tant que la valeur de l'expression reste non nulle. On teste l'expression avant d'exécuter l'instruction.


Sémantique dénotationnelle On associe un objet mathématique (abstrait) à chaque expression. Par exemple, on construit un modèle mathématique des entiers, muni des opérations sur les entiers. C'est beaucoup plus dur pour les fonctions (calculables).


Sémantique opérationnelle On définit un ensemble de valeurs (ou résultats) puis une relation d'évaluation qui relie des programmes avec des résultats.

Avantages et inconvénients


Sémantique informelle Facile à comprendre, synthétique, mais parfois imprécise. Impossible de faire des preuves.


Sémantique dénotationnelle La vérité selon les maths. Très complexe, difficile à exploiter.


Sémantique opérationnelle Plus facile à manipuler, c'est aussi celle qui nous intéresse le plus souvent (pour calculer). Ce n'est pas tout à fait la réalité mathématique.


Note :

Interpréteur pour la calculette

    type valeur = int

    let cherche x env = List.assoc x env

    let rec evalue env = function
      | Const n -> n
      | Variable x -> cherche x env
      | Bin (op, e1, e2) ->

          let v1 = evalue env e1 and v2 = evalue env e2 in
          begin match
 op with
           | Plus -> v1 + v2 | Moins -> v1 - v2
           | Mult -> v1 * v2 | Div -> v1 / v2

          end


Une présentation plus neutre (S.O.S.)



(Sémantique Opérationnelle Structurelle)


Une autre présentation plus mathématique et régulière consiste à définir une relation ρ ⊢ ev qui se lit ``Dans l'environnement ρ, l'expression e s'évalue en la valeur v'' par des règles d'inférence.

Une règle d'inférence est une implication P1P2 ∧ … ∧ Pk C présentée sous la forme
P1           P2           …           Pk
C
que l'on peut lire pour réaliser (évaluer) C il faut réaliser à la fois P1 et ... Pk.

Sémantique des expressions arithmétiques

Les jugements de la forme ρev
ρ Const nn     
xdom(ρ)
ρ Variable x ρ(x)
ρ e1v1           ρ e2v2
ρ Bin (Plus , e1, e2) v1 + v2
    
ρ e1v1           ρ e2v2
ρ Bin (Times , e1, e2) v1 * v2


Une construction de liaison

On étend la calculette avec des liaisons « let ».
    | Let of string * expression * expression
avec, par exemple, la syntaxe concrète
    "let" VARIABLE "=" expression "in" expression
L'expression Let (x, e1, e2) lie la variable x à l'expression e1 dans l'évaluation de l'expression e2.

Formellement:
ρ e1v1          ρ, xv1e2v2
ρ Let (x, e1, e2) v2
où ρ, xv ajoute la liaison de x à v dans l'environnement ρ en cachant une ancienne liaison éventuelle de x.

Modification de l'interprète

    let ajoute x v env = (x,v)::env

    let rec evalue env = function
      ...
    | Let (x, e1, e2) ->
        let v1 = evalue env e1 in
        evalue (ajoute x v1 env) e2
Noter le codage de ρ, xv par (x, v):: ρ.

Exercise 1   Soit let x = 1 in (let x = 2 in x) + x,


Arbre de dérivation

L'ensemble des réponses (de l'exercice précédent) est contenu dans l'arbre de dérivation, qui constitue une preuve de l'évaluation de l'expression en la valeur 3
Const 1 1          
x ↦ 1Const 2 2           x ↦ 1, x ↦ 2x 2
x ↦ 1Let (x, Const 2, x) 2
          x ↦ 1x 1
x ↦ 1Bin (Plus , Let (x, 2, x), x) 3
Let (x, Const 1, Bin (Plus , Let (x, 2, x),x)) 3


Formalisation des erreurs

L'évaluation peut mal se passer, même dans la calculette, division par 0, accès à une variable inconnue, etc.

La sémantique opérationnelle peut formaliser les erreurs ou pas.

On remplace la relation ρ ⊢ ev par une relation ρ ⊢ err est une réponse. Les réponses sont l'union des valeurs v ou des erreurs z.
ρ e1v1                    ρ e2v2    v2 ≠ 0
ρ Bin (Div , e1, e2) v1 / v2
ρ e2 0
ρ Bin (Div , e1, e2) Division 
Le type des erreurs (par exemple):
type erreur = Division_par_zéro | Variable_libre of string


Implémentation des erreurs


Formellement, on devrait utiliser un type somme
type résultat = Valeur of valeur | Erreur of erreur

let rec evalue env = function 
    ...
  | Bin (Div, e1, e2) -> 
begin match  evalue env e2 with
      | Erreur _ as e -> e
      | Valeur 0  -> Erreur Division_par_zéro
      | Valeur v2 ->

          begin match evalue env e1 with
          | Erreur _ as e  -> e
          | Valeur v1      -> Valeur (v1 / v2)

          end end



En pratique, on utilise les exceptions du langage hôte
exception Erreur of erreur
let erreur x = raise (Erreur x)
let cherche x l =
  try List.assoc x l
  with Not_found -> erreur (Variable_libre x)

let rec evalue env = function 
    ...
  | Bin (Div, e1, e2) ->

      let v2 = evalue env e2 in
      if
 v2 = 0 then
        erreur (Division_par_zéro)
      else
        let
 v1 = evalue env e1 and v2 = evalue env e2 in

        v1 / v2


Terminaison

L'évaluation peut ne pas terminer.


La S.O.S (à grands pas) ne permet ne modélise pas la terminaison, un programme qui ne termine pas ne peut pas être mis en relation avec une réponse.


Sémantique à réduction à petits pas

Dans le cours Langage et programmation on définit une sémantique opérationnelle à réduction ``à petits pas'' qui permet quand même de décrire le calcul des programmes qui ne terminent pas : le calcul est modélisé par une relation de réduction interne, i.e. les programmes se réduisent sur eux mêmes (chaque étape élémentaire du calcul est modélisé par un petit pas de réduction) jusqu'à plus soif ou indéfiniment.

Constructions impératives

Les constructions impératives (variables « mutables », entrées sorties, etc.) sont exécutées pour leur effet.

Les expressions sont évaluées pour leur valeur.

Il est commode (mais pas obligatoire, cf. Caml) de distinguer instructions et expressions.
type instruction =
| Affecte 
of string * expression
| Sequence 
of
 instruction * instruction
Avec pour syntaxe concrète :
instruction ::= VARIABLE ":=" expression
            |   instruction ";" instruction


Sémantique des instructions

La mémoire σ associe les adresses l aux valeurs v, l'environnement ρ associe les variables x aux adresses.

L'exécution d'une instruction i est rendue par un jugement ρ / σ i ⇒ / σ' qui se lit dans l'état-mémoire σ et l'environnement ρ, l'exécution de l'instruction i produit un nouvel état mémoire σ'.
ρ / σi1 ⇒ / σ1    ρ / σ1i2 ⇒ / σ2
ρ / σSequence (i1, i2) ⇒ / σ2
xdom(ρ)    ρ(x) ∈ dom(σ)    ρ / σ ev
ρ / σAffecte (x, e) ⇒ / σ, ρ(x) ↦ v
xdom(ρ)   ρ(x) ∈ dom(σ)
ρ / σ Variable x σ(ρ(x))


Interprétation des instructions

Exercise 2   L'interpréteur.
(* Les adresses sont les adresses de Caml *)
type environnement = (string * valeur ref) list

let rec evalue env = function
   ...
  | Variable x -> !(cherche x env)


and execute env = function
| Sequence (i1, i2) -> execute env i1 ; execute env i2
| Affecte (x, e)    ->

    let v = evalue env e in
    let
 cell = cherche env x in

    cell := v


Les booléens, la conditionnelle

Les booléens sont true et false. Le domaine des valeurs est alors la réunion de l'ensemble des entiers et de celui des booléens. En toute rigueur les types doivent apparaître dans les règles de SOS :
ρ e1v1           ρ e2v2
ρ Bin (Plus , e1, e2) int(→int(v1) + →int(v2))


C'est assez lourd, on s'en passe.
ρ e1true           ρ e2v
ρ If (e1, e2, e3) v
    
ρ e1false           ρ e3v
ρ If (e1, e2, e3) v

Interpréteur

Mais en Caml, c'est l'inverse: on distingue facilement valeurs entières et booléennes.
type expression = … | If of expression * expression * expression

type valeur = Int of int | Bool of bool

let rec evalue env = function
| Const n -> Int n
        ⋮
| If (e1, e2, e3) ->

    match  evalue env e1 with

    | Bool true  -> evalue env e2
    | Bool false -> evalue env e3

Les opérations sur les booléens

Deux sémantiques Laquelle semble préférable ?

Sur l'ordre d'évaluation

La SOS semble parfois ne rien en dire. Mais...

Doit-on spécifier l'ordre d'évaluation ?

Oui
(Java, Pseudo-Pascal) La sémantique est non-ambigüe.
Non
(C, Caml) L'auteur du compilateur peut optimiser.

En pratique de programmation
Éviter :
 x = 10 * (getchar() - '0') +  getchar() - '0' ;


Adopter :
c1 = getchar() - '0' ;
c2 = getchar() - '0' ;
x = 10 * c1 + c2 ;


Les tableaux

Pour simplifier : tableaux alloués explicitement.

On ajoute trois constructions à la syntaxe :


Construction proche des tableaux de Java (et Caml), quelle différence ?

Sémantique des tableaux


Allocation
ρ / σ e1k           k ≥ 0                    t = (0 ↦ l0,…, k-1 ↦ lk-1)           l0dom(σ)… lk-1dom(σ)
ρ / σ Affecte (x, Alloc (e1)) ⇒ / σ, l0↦⊥,… lk-1↦⊥, ρ(x) ↦ t



Lecture
ρ / σ e1t           ρ / σ e2k           kdom(t)
ρ / σ Lire (e1, e2) σ (t(k))



Écriture
ρ / σ e1t           ρ / σ e2k           kdom(t)           ρ / σ e3v
ρ / σ Ecrire (e1, e2, e3) ⇒ / σ, t(k)↦ v


Interprétation des tableaux, syntaxe et valeurs

type expression = ...
| Alloc of expression
| Lire of expression * expression
and instruction = ...
| Ecrire expression * expresssion * expression
type valeur = Int of int | Array of valeur array | Undefined
type erreur = ... | Type | Index

let array_of_valeur = function

| Array t -> t
| _       -> erreur Type



Code des expressions
let rec evalue env = function
    ...
  | Alloc (e1) ->
      let k = int_of_valeur (evalue env e1) in
      if
 k >= 0 then
        Array (Array.create k Undefined)
      else
        erreur Index
  | Lire (e1, e2) ->
      let t = array_of_valeur (evalue env e1)
      and k = int_of_valeur (evalue env e2) in
      if
 0 <= k && k < Array.length t then
        t.(k)
      else
        erreur Index



Code des instructions
and execute env = function
  ...
| Ecrire (e1, e2, e3) ->

    let t = array_of_valeur (evalue env e1)
    and k = int_of_valeur (evalue env e2)
    and v = evalue env e3 in
    if
 0 <= k && k < Array.length t then
        t.(k) <- v
      else

        erreur Index


Fonctions globales

Dans la calculette : un programme est une suite de définitions de fonctions, plus une expression.
fact(x) = if x=0 then 1 else x * fact (x-1) ;;
fact(10)



Important Les seules variables du corps d'une fonction sont :

type expression = ...
| App of string * expression

type fonction = Fun of string * expression (* Pas une expression *)

type programme =
  {fonctions : (string * fonction) list ;
  expr : expression}


Sémantique des fonctions globales


Évaluation des programmes
On évalue {fonctions=ρf; expr=e} en prouvant le jugement :
ρf;  ∅ev



Règle de l'application
ρf(f) = Fun (x,ef)           ρf;  ρeva           ρf;  (xva)efv
ρf;  ρApp (f, e)v



Note L'environnement pour évaluer le corps ef.


Re-Note En pratique, il peut y avoir plus d'un argument et les fonctions déclarent des variables locales.

Interpréteur

let rec evalue fenv env = function
        ...
| App (f, e) ->
    let va = evalue fenv env e in
    let
 Fun (x,ef) = cherche fenv f in
    evalue fenv [x, va] ef

let exécute_programme {fonctions=fenv ; expr=e} =
  evalue fenv [ ] e



Note Ou pourrait abstraire un peu l'environnement (un seul env, des fonctions cherche_fun et cherche_val).

Fonctions et construction impératives

(Au moins) deux règles possibles.


Appel par valeur
ρf(f) = (x,ef)           ρf;  ρ / σeva
          ladom(σ)           ρf;  (xla) / σ, lavaefv
ρf;  ρ / σApp (f, e)v



Appel par variable
ρf(f) = (x,ef)           ρ(y) = la           ρf;  (xla) / σefv
ρf;  ρ / σApp (f, y)v



Vocabulaire La variable est le paramètre formel l'argument le paramètre effectif.

Paramètres tableaux


Appel par valeur de nos tableaux
ρf(f) = (x,ef)           ρf;  ρ / σet           t = (0 ↦ l0,…, k-1 ↦ lk-1)
ladom(σ)           ρf;  (xla) / σ, latefv
ρf;  ρ / σApp (f, e)v


Connaissez vous le passage des tableaux, en Java, en C, en Pascal ?

Il y a une grosse différence: si on passe un tableau à une fonction Pascal, la fonction possède un nouveau tableau! Alors, comment comprendre:
En C, en Java et en Pascal, la règle (par défaut) est le passage par valeur ?

Bonus : la calculette fonctionnelle


Difficultés engendrées
  1. Les fonctions sont définies dans un environnement ρ mais elles sont appelées dans un environnement ρ' en général différent;

    La liaison lexicale exige que la valeur des variables soit prise au moment de la définition.
  2. Si les fonctions sont des valeurs (retournées en résultat), alors leur durée de vie peut excéder leur portée lexicale.

Exemple


Liaison statique
let x = 3             (* env1 : x est lié à une valeur v  *)
let rec mem =         (* la valeur de x est celle de env1 *)
  function [ ] -> false | h::t -> x = h && mem t
let x = w             
(* env2 : x est lié à la valeur w   *)
mem l                 (* doit être évalue; dans env1      *)



Valeur fonctionnelles
let incr x = fun y -> x + y
                    (* incr x retourne une fonction       *)
let f = incr 3      (* f doit mémoriser la valeur 3 de x  *)
f 4                 (* pour la restaurer lors de l'appel  *)


Fermetures


Une solution générale consiste à évaluer les fonctions en des fermetures.

Une fermeture est une paire Fun (x, e),ρ⟩ formée d'une expression fonctionnelle Fun (x, e) et d'un environnement ρ.
ρ Fun (x, e) Fun (x, e),ρ⟩
On peut éventuellement restreindre ρ à l'ensemble des variables libres dans Fun (x, e). Elle permet d'enfermer une expression avec son environnement statique. Une fermeture est une valeur qui peut être passée en argument.


L'application restore l'environnement statique.
ρe1Fun (x, e0),ρ0           ρe2v2           ρ0, xv2e0v
ρApp (e1, e2)v


Variables libres

Un variables libre d'une expression est une variable utilisée dans cette expression dans un contexte où elles ne sont pas liée:
vl(Const n) = ∅     vl(App (e1, e2)) = vle1vle2
vl(Variable x) = { x }     vl(Plus (e1, e2) = vle1vle2
vl(Fun (x, e)) = vle1 \ { x }     ...



Exemple La seul variable libre de (fun y -> x + (fun x x) y) est x. En effet y apparaît dans une contexte dans lequel elle est liée. Par contre, x apparaît au moins une fois dans un contexte dans lequel elle n'est pas liée.


Fermetures

Il suffit de mettre dans une fermeture les variables libres. On peut remplacer e,ρ⟩ par e,ρ / vl(e)⟩.

Fonctions locales non retournées

Les fonctions locales non retournées sont un cas intermédiaire.

On peut éviter les fermetures par remontée des variables.


Principe: une fonction f =def Fun (x, e) avec une variable libre y locale à une fonction globale g peut être transformée en une fonction globale f' =def Fun ((x, y), e). Les appels à f de la forme App (f, e) sont transformés en App (f, e, y).

Il faut si besoin renommer d'autres liaisons locale de la variable y qui cacheraient la liaison globale y.


Note: Comme les deux programmes ont la même sémantique, on peut utiliser la transformation inverse si le langage le permet, et écrire des programmes où les variables sont ``descendues''.

Exemple

Deux styles de programmation différents.

Variables remontées
let member (x,l) =
  let rec mem l =
    match l with
    | []   -> false
    | h::t ->
        x = h || mem t
  in mem l
  Variables descendues
let member (x,l) =
  let rec mem (x,l) =
    match l with
    | []   -> false
    | h::t ->
       x = h || mem (x,t) in
  mem (x,l)
En pratique les deux écritures sont utiles (selon le contexte, et le nombre d'arguments, préciser).

Un compilateur peut effectuer l'une ou l'autre des transformations.

Le langage Pseudo-Pascal (PP)

C'est à vous d'écrire un interpréteur, en grappillant dans ce cours et en utilisant votre culture. Voir aussi (et surtout) le poly.


Ce document a été traduit de LATEX par HEVEA