Vous êtes sur la page 1sur 45

Introduction, Langage de réalisation

DEUG MIAS M2

Introduction - Langage de réalisation

1. Objectifs du cours
1. Fonctionnel vs Impératif
2. Exemple
3. Langage de réalisation
1. Actions élémentaires
1. Affectation
2. Lecture
3. Ecriture
2. Actions composées
1. Séquence
2. Découpage par cas
3. Itération
3. Actions nommées et fonctions
1. Actions nommées
2. Fonctions

Objectifs du cours

Les objectifs de ce cours sont :


● Présentation du style impératif de programmation ;

● Apprentissage d'un langage impératif : Pascal (essentiellement en TD) ;

● Retour sur les notions de type et de variable ;

● Notion d'abstraction - structures de données et algorithmes associés ;

● Mécanismes d'entrées/sorties ;

● Syntaxe et sémantique.

Fonctionnel vs Impératif

Dans l'approche fonctionnelle, on compose des fonctions qui prennent des arguments et retournent des valeurs.
Les programmes sont décrits à l'aide de données et de traitements.
● donnees décrites par leurs types ;

● traitements décrits par des fonctions.

Les types sont définis à partir de :


● types de base ;

● constructeurs de types (produit, union, liste ...).

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/1-intro-lge_real.html (1 of 7) [23-09-2001 13:40:07]


Introduction, Langage de réalisation

Les fonctions sont définies par :


● un profil (c'est tout ce dont on a besoin pour se servir de la fonction)

● une structure (une expression)

Les noms que l'on utilise représentent :


● des fonctions ;

● des arguments (on dit aussi paramètres formels) ;

● des expressions (par le mécanisme d'abstraction).

On utilise le terme de variable, dans un sens mathématique : sous la portée de l'abstraction, toute instance d'une variable x
représente la même valeur, qui a été liée à la variable dans la déclaration de l'abstraction.
Dans l'approche impérative, on décrit aussi les programmes à l'aide de données et de traitements.
● les données sont décrite par des types;

● les traitements sont décrits par des fonctions et des actions (procédures).

Les types sont définis à partir de :


● types de base ;

● constructeurs de types (différents de ceux de l'approche fonctionnelle).

Les fonctions et actions sont définies par :


● un profil

● une structure (une séquence d'actions élémentaires ou composées, appelées aussi instructions.

Les noms représentent :


● des fonctions et des actions ;

● des arguments ;

● des variables.

Ces variables sont différentes des variables de la programmation fonctionnelle. Ce ne sont pas des variables au sens mathématique.
Chaque variable a une valeur, mais qui peut changer au cours du temps, à l'intérieur de son domaine de portée. On attribue une
valeur à une variable par l'affectation. On peut visualiser une variable comme une boîte, qui a une étiquette (son nom) et un contenu
(sa valeur).
En mode impératif, on peut écrire :
x <- 2
y <- 2*x + 1
x <- y * y

Il n'y a pas d'équivalent en programmation fonctionnelle.


La programmation impérative est plus proche du fonctionnement réel de la machine (on peut voir une variable comme l'étiquette
d'une partie de la mémoire), tandis que la programmation fonctionnelle est plus proche d'un modèle mathématique de calcul, donc
plus "abstraite".
Exemple

Nous allons illustrer ces concepts de base par un petit exemple, la résolution du trinôme du second degré.
action RésoudreTrinôme
{ lit 3 réels a, b, c, a != 0, et écrit les solutions réelles de l'équation ax2 + bx
+ c = 0 }
lexique
a, b, c, d : réel
début
lire_non_nul (a)
lire (b)

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/1-intro-lge_real.html (2 of 7) [23-09-2001 13:40:07]


Introduction, Langage de réalisation
lire (c)
d <- discriminant (a, b, c)
selon le cas d
d < 0 :
écrire ("pas de solution")
d = 0 :
écrire ("une solution double")
écrire (-b / 2a);
d > 0 :
écrire ("deux solutions")
écrire (-b - sqrt(d) / 2a)
écrire (-b + sqrt(d) / 2a);
fin

action lire_non_nul (résultat x : réel)


{ lit un entier non nul au clavier }
début
répéter
lire (x)
jusqu'à x != 0
fin

fonction discriminant (a, b, c : réel) -> réel


{ calcule le discriminant d'un trinôme du second degré de coéfficients a, b, c }
début
retourner b2 - 4ac
fin

A titre de comparaison, une version fonctionnelle de cet exemple pourrait être (sans les entrées-sorties) :
type solution
{ représente les solutions possibles d'un trinôme }
pas_de_solution
une_solution : réel -> solution
deux_solutions : réel * réel -> solution

RrésoudreTrinôme (a, b, c) :
soit d = discriminant (a, b, c)
dans :
selon le cas d
d < 0 : pas_de_solution
d = 0 : une_solution (- b / 2a)
d > 0 : deux_solutions (-b - sqrt(d) / 2a,
-b + sqrt(d) / 2a)

Discriminant (a, b, c) : b2 - 4ac

Langage de réalisation

Pour permettre de prendre en compte les concepts de la programmation impérative, le langage de réalisation a été modifié par
rapport à celui vu en M1 :
● déclaration d'actions ;

● déclaration des types des objets (arguments, variables) ;

● actions élémentaires : affectation, lire, écrire ;

● actions composées : répéter, retourner.

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/1-intro-lge_real.html (3 of 7) [23-09-2001 13:40:07]


Introduction, Langage de réalisation
On va donc décrire plus précisement ce langage de réalisation :
Actions élémentaires

Affectation

Le symbole d'affectation "<-" se lit "reçoit"


a <- 2
val <- 2x + 1

Lecture

Lire une donnée entrée au clavier. Selon le type de x, seuls sont acceptés des nombre entiers ou réels.
lire (x)

Ecriture

Ecrire un message, un entier ou un réel à l'écran.


écrire ("message")
écrire (x)
écrire (2x + 1)

On peut également mettre plusieurs arguments :


écrire ("x = ", x)

est équivalent à :
écrire ("x = ")
écrire (x)
Actions composées

Séquence

Les actions d'une séquence sont exécutées l'une après l'autre :


lire (x); lire (y); écrire (x+y)

ou
lire (x)
lire (y)
ecrire (x+y)

ou
lire (x); lire (y)
ecrire (x+y)

Découpage par cas

Comme dans le langage de réalisation vu au M1 :


selon le cas x
x < 0 : écrire ("négatif")

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/1-intro-lge_real.html (4 of 7) [23-09-2001 13:40:07]


Introduction, Langage de réalisation
x = 0 : écrire ("nul")
x > 0 : écrire ("positif")

selon le cas x
x > 0 : écrire ("négatif")
x = 0 : écrire ("nul")
sinon : écrire ("positif")

si pair (x)
alors x <- x - 1
sinon x <- 3x + 2

si x < 0
alors x <- -x

Itération

Dans une itération, le corps est exécuté 0, 1 ou plusieurs fois en fonction du contrôle de la boucle. Si le corps est exécuté une
infinité de fois, on dit que le programme "boucle". C'est une erreur de conception ou de programmation.
Dans un "tantque", le corps est exécuté tant que la condition est vraie :
tantque pair (x)
x <- x / 2

Dans un "répéter", le corps est exécuté jusqu'à ce que la condition soit vraie :
répéter
lire (x)
jusqu'à x != 0

Pour qu'une itération de type "tantque" (resp. "répéter") ne boucle pas, il faut s'assurer que la condition peut devenir fausse (resp.
vraie). Il faut donc au moins que le corps de la boucle modifie des variables qui sont utilisées dans la condition.
Cette itération boucle indéfiniement si x est positif.
tantque x > 0
écrire (x)

Dans un "pour", le corps est exécuté un nombre pré-déterminé de fois :


s <- 0
pour i parcourant [1..10]
s <- s + i

pour i parcourant [0..5] en sens inverse


écrire (i, "...")

Les itérations "pour" ne peuvent pas boucler. Il est interdit de modifier la valeur de la variable de boucle dans le corps de la boucle.
Les bornes de l'intervalle sont évaluées une seule fois, au début de la boucle. La boucle suivante est exécutée exactement 10 fois :
x <- 10
pour i parcourant [1..x]
x <- 20
Actions nommées et fonctions

Actions nommées

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/1-intro-lge_real.html (5 of 7) [23-09-2001 13:40:07]


Introduction, Langage de réalisation

Le nommage est un mécanisme d'abstraction : il permet de créer une boîte noire (l'action nommée) qui peut être utilisée comme une
action élémentaire.
action démo (x, y : entier)
lexique
{ déclaration des variables utilisées }
z : réel
début
{ corps de l'action }
z <- x + y
écrire (z)
fin

Les variables déclarées dans le lexique ne sont utilisables que dans le corps.
Appels de l'action :
démo (1, 2)
démo (n + 1, p * 2)

Le nombre et le type des arguments utilisés dans un appel doivent correspondre à ceux utilisés dans la déclaration de l'action.
Dans une action nommée, les arguments peuvent être :
● des données : leur valeur est transmise à l'action lors de l'appel ;

● des résultats : leur valeur est calculée par l'action et retransmise à l'appelant à la fin de l'invocation ;

● des données-résultats : leur valeur est transmise à l'action lors de l'appel, éventuellement modifiée dans l'action, et
retransmise à l'appelant à la fin de l'invocation.
Illustration :
● je donne une lettre à quelqu'un, qui va la poster pour moi : passage de données ;

● je demande à une personne de me ramener mon courrier : passage de résultat ;

● je fais signer une lettre par une personne : passage de donnée-résultat.

Les arguments de données sont les plus fréquents. Les arguments de résultat et de donnée-résultat doivent être explicitement
déclarés dans la déclaration de l'action :
action div (a, b : entier; résultat q, r : entier)
début
q <- 0
tantque a > b
a <- a - b
q <- q + 1
r <- b
fin

action échange (donnée-résultat x, y : entier)


lexique
t : entier
début
t <- x
x <- y
y <- t
fin

Lors de l'invocation, les arguments qui sont déclarés résultat ou donnée-résultat doivent être des variables :
div (15, 4, quotient, reste)
échange (quotient, reste)

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/1-intro-lge_real.html (6 of 7) [23-09-2001 13:40:07]


Introduction, Langage de réalisation

Fonctions

Les fonctions peuvent être définies comme les actions nommées :


fonction fact (n : entier) -> entier
lexique
res : entier
début
si n = 0
alors res <- 1
sinon res <- n * fib (n - 1)
retourner res
fin

L'action retourner n'est valide que dans une fonction. Elle permet d'indiquer la valeur retournée par la fonction. Il est recommandé
que retourner n'apparaissent que comme dernière action du corps de la fonction.
Contrairement aux actions, les fonctions n'acceptent que des arguments de données.

Autres chapitres :
1. Introduction - Langage de réalisation (ce chapitre)
2. Structures de données
3. Lexique et syntaxe
4. Sémantique

Michel Beaudouin-Lafon, mbl@lri.fr

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/1-intro-lge_real.html (7 of 7) [23-09-2001 13:40:07]


Structures de données

DEUG MIAS M2

Structures de données

1. Types abstraits et Types concrets


1. Types abstraits
1. Exemple :
2. Types concrets
2. Types de base
1. Types énumérés
2. Types numériques
3. Types intervalles
3. Types composés
1. Types produit
2. Types tableau
1. Tableaux multi-dimensionnels
2. Exemple 1 - type Liste
3. Exemple 2 - type Graphe
3. Types somme

Types abstraits et Types concrets

Un type est une classe de données ayant des propriétés communes. Les types sont utilisés pour aider à vérifier la
correction des programmes, pour traduire les programmes en code exécutable (compilation), etc. C'est une notion
fondamentale de l'informatique.
Dans la suite on va parler de types abstraits et de types concrets. On utilise les types abstraits pour la réalisation et les
types concrets pour le codage. Les types concrets dépendent donc du langage de programmation que l'on utilise.
Types abstraits

Un type abstrait est définit par :


● un nom ;

● un ensemble de constructeurs (constantes ou fonctions) qui permettent de produire des données du type abstrait à
partir d'autres données ;
● des opérateurs qui permettent de manipuler les données du type abstrait ;

● parfois, des invariants qui caractérisent les données du type abstrait.

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (1 of 11) [23-09-2001 13:40:45]


Structures de données

Exemple :

Par exemple, on peut parler du type polynôme car tout polynôme peut être représenté par un degré et une suite de
coefficients ci :

P(x) = cnxn + cn-1xn-1 + ... + c1x + c0

On peut avoir définir deux constructeurs :


● le polynôme constant (degré 0)

● une fonction qui prend un polynôme P et un terme constant C et construit le polynôme P.x + C

Les opérations sur les polynômes peuvent être par exemple :


● l'addition, la multiplication par une constante, le quotient, etc.

● l'évaluation pour un x donné, etc.

La déclaration peut se faire de la façon suivante :


type polynôme
{ constructeurs }
polynôme_nul
construire : polynôme * réel -> polynôme
{ opérations }
ajouter : polynôme * polynôme -> polynôme
éval : polynôme * réel -> réel
Types concrets

Les types concrets sont les types utilisés dans les langages de programmation. Alors qu'un type abstrait ne fait aucune
supposition sur la représentation (le codage) des données du type, un type concret définit précisément l'implantation en
machine des donnés de ce type, et les opérations prédéfinies qui sont disponibles.
Par exemple, un type abstrait "entier naturel" représente tout nombre >= 0, mais le type concret correspondant peut ne
coder que les entiers entre 0 et 65535.
Les constructeurs et les opérations du type abstrait sont :
● soit codées par des fonctions et des procédures du langage ;

● soit codées directement par les opérations prédéfinies du type concret.

Dans le premier cas on parle de codage explicite et dans le second de codage implicite (vu en M1).
Dans un langage impératif, il faut déclarer le type de chaque variable, argument et valeur de retour de fonction du
programme. Cela permet de vérifier que les constructions du programme sont correctes du point de vue des types. Nous
reviendrons sur ce point en détail plus tard. Par exemple :
● affectation v := expr. Il faut que v et expr soient de même type (entier par exemple). Dans certains cas on
accepte des types compatibles (entier et réel par exemple).
● conditionnelle, itérations tantque et répéter : l'expression doit avoir un type booléen (vrai ou faux).

Les types concrets d'un langage de programmation sont


● les types de base (primitifs)

● les types composés (structurés)

Dans la suite, on décrit un ensemble de types abstraits de base et composés qui correspondent (à peu près) aux types
concrets usuels des langages impératifs. L'usage de ces types abstraits permettra de faciliter le codage implicite, qui est en
général plus efficace à exécuter et plus concis à écrire.

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (2 of 11) [23-09-2001 13:40:46]


Structures de données

Types de base

Les types de base sont les types les plus simples et correspondent généralement aux entités manipulées directement par la
machine.
Types énumérés

Un type énuméré est défini par un domaine fini, décrit par l'énumération de ses éléments :
type carte
{constantes: }
pique, coeur, carreau, trèfle

On utilisera la notation plus concise suivante :


type carte = (pique, coeur, carreau, trèfle)

Les constante d'un type énuméré sont ordonnées, selon l'ordre de leur déclaration. Cela permet de définir un constructeur
et 3 opérations sur tout type énuméré :
image : entier -> carte { constructeur }
ord : carte -> entier { indice dans l'énumération }
suivant : carte -> carte { élément suivant }
précédent : carte -> carte { élément précédent }

Cela nous permet de définir plusieurs invariants :


image(ord(x)) = x
image(x) défini => ord(image(x)) = x
suivant(x) = image(ord(x) + 1)
précédent(x) = image(ord(x) - 1)

Le type booléen est prédéfini avec les valeurs faux et vrai. Les opérations de l'algèbre de Boole sont définies sur les
booléens :
et : booléen * booléen -> booléen
ou : booléen * booléen -> booléen
non : booléen -> booléen

Le type caractère est prédéfini et représente l'ensemble des caractères imprimables. On note les constantes de ce type
entre quotes :
'a', 'b', ..., 'z', '0', ..., '9', ';', '$', ...
Types numériques

Les types entier et réel sont prédéfinis. Les opérations arithmétiques et logiques usuelles sont définies sur ces types.
Un entier peut être converti en réel et un réel peut être converti en entier (par troncature de la partie décimale).
Il y a une différence importante entre les types numériques abstraits, qui correspondent aux ensembles Z et R, et les types
concrets, qui ne représentent qu'un ensemble fini de valeurs.
Types intervalles

On définit un type intervalle à partir d'un type énuméré ou du type entier par la notation suivante :

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (3 of 11) [23-09-2001 13:40:46]


Structures de données

type rouge = couleur dans [coeur .. carreau]


type lettres = caracrtère dans ['a' .. 'z']
type dizaine = entier dans [10 .. 19]

Un type intervalle représente l'ensemble des constantes comprises entre les deux bornes de l'intervalle. Cette définition a
un sens puisque les constantes d'un type intervalle sont toujours ordonnées.
Des règles particulières s'appliquent pour que les valeurs d'un type intervalle soient converties en valeur de leur type de
base. Elles seront décrites plus tard.
Types composés

Les types composés (appelés aussi types structurés) sont définis à partir d'un constructeur de type et d'un ou plusieurs
types composants. Nous allons voir :
● les types produit ;

● les types tableau ;

● les types somme.

Types produit

Un type produit est construit à partir d'un ensemble de composants. Chaque composant peut être extrait par une opération
dite de projection :
type complexe
{ constructeur }
creer_complexe : réel * réel -> complexe
{ operations }
partie_réelle : complexe -> réel
partie_imaginaire : complexe -> réel

On appelle ce type un type enregistrement ou structure, les composants s'appellent des champs et on le note :
type complexe : produit
re : réel
im : réel
fin

La notation pointée permet d'accéder aux champs


c.re = partie_réelle (c)
c.im = partie_imaginaire (c)

On peut définir d'autres opérations sur les nombres complexes, comme le module et l'argument. (Noter que dans ce cas, on
aurait pu représenter également un nombre complexe par son module et son argument, et avoir des opérations retournant
les parties réelles et imaginaires.)
Les champs d'un enregistrement peuvent apparaitre en partie gauche d'une affectation :
c.re <- 1.0

Cela est équivalent à utiliser une nouvelle opération :


changer_partie_réelle : complexe * réel -> complexe

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (4 of 11) [23-09-2001 13:40:46]


Structures de données

qui vérifie l'invariant :


partie_réelle (changer_partie_réelle (c, r)) = r.

L'affectation
c.re <- 1.0

est équivalente à
c <- changer_partie_réelle (c, 1.0)

Si l'on veut que le programme vu au premier cours puisse calculer des racines complexes, il suffit de modifier le cas d < 0
:
lexique
c1, c2 : complexe
début
...
selon le cas d
...
d < 0 :
c1.re <- -b / 2*a
c1.im <- sqrt(d) / 2*a
c2.re <- -b / 2*a
c2.im <- - sqrt(d) / 2*a

Types tableau

Le type tableau permet de représenter des fonctions à domaine fini : à chaque valeur d'un indice, le tableau associe une
valeur ou élément de tableau.
Par exemple, on peut représenter les valeurs des cartes dans un jeu par :
type carte = (roi, dame, valet, as)
type valeur_carte
{ opérations }
valeur : tableau * carte -> entier
change_valeur : tableau * carte * entier -> tableau

A chaque carte, le tableau fait correspondre une valeur entière, accessible par l'opération valeur. On peut changer la
valeur qui correspond à une carte par l'opération change_valeur. On peut se représenter un tableau comme un
bloc-tiroirs dont chaque tiroir a une étiquette correspondant à son indice dans le tableau et dont le contenu est la valeur.
Chaque tiroir est appelé un élément du tableau.
Un invariant de tableau est le suivant :
valeur (change_valeur (t, c, x), c) = x

Pour faciliter l'usage des tableaux, on les note comme suit :


type carte = (roi, dame, valet, as)
type valeur_carte = tableau [carte] de entier

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (5 of 11) [23-09-2001 13:40:46]


Structures de données

Le type de l'indice (ici carte) doit être un type énuméré ou intervalle. Le type des éléments (ici entier) peut être
quelconque. Pour accéder aux éléments d'un tableau, on utilise la notation indicée :
t[i] = valeur (t, i)

Un élément de tableau peut apparaitre en partie gauche d'une affectation, ce qui permet de changer sa valeur :
t[i] <- v

est équivalent à
t <- changer_valeur (t, i, v)

L'accès à un élément de tableau n'est légal que si l'indice est compris dans l'intervalle défini à la déclaration du tableau.
Avec l'exemple des cartes :
type carte = (roi, dame, valet, as)
lexique
points : tableau [carte] de entier
main : tableau [1..5] de carte
total : entier
début
points[roi] <- 10
points[dame] <- 9
points [valet] <- 8
points [as] <- 1
pour i parcourant [1..5] faire
main [i] <- carte_hasard

total <- 0
pour i parcourant [1..5] faire
total <- total + points[main[i]]

Lorsque l'on utilise comme indice un type intervalle d'entier, on retrouve pratiquement la notation indicée usuelle en
mathématiques :
type vecteur = tableau [1..10] de réel
somme_vecteur = vecteur * vecteur -> vecteur
produit_scalaire = vecteur * vecteur -> réel

fonction somme_vecteur (v1, v2)


lexique
v : vecteur
i : 1..10
début
pour i parcourant [1..10]
v[i] <- v1[i] + v2[i]
retourner v

fonction produit_scalaire (p, q)


lexique
produit : réel
i : 1..10
début

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (6 of 11) [23-09-2001 13:40:46]


Structures de données

produit <- 0
pour i parcourant [1..10]
produit <- produit + p[i] * q[i]
retourner produit

Cet exemple montre que l'on peut considérer le type tableau comme un type produit particulier. Pour des vecteurs à 3
dimensions, au aurait pu écrire :
type vecteur3D = produit
x, y, z : réel
fin
somme3D = vecteur3D * vecteur3D -> vecteur3D

fonction somme3D (v1, v2)


lexique
v : vecteur3D
début
v.x <- v1.x + v2.x
v.y <- v1.y + v2.y
v.z <- v1.z + v2.y
retourner v

On voit que l'écriture est plus lourde, surtout si l'on avait des vecteurs à 10 dimensions. On remarque cependant la
similitude entre la notation pointée et la notation indicée.

Tableaux multi-dimensionnels

On peut définir des tableaux multi-dimensionnels (ou matrices) en utilisant le fait que les éléments d'un tableau peuvent
eux-même être des tableaux :
type matrice= tableau [1..4] de tableau [1..4] de réel

L'accès se fait alors par :


lexique
m : matrice
début
m[1][1] <- 0

Pour simplifier la notation, on peut écrire :


type matrice = tableau [1..4, 1..4] de réel
lexique
m : matrice
début
m[1, 1] := 0

Exemple 1 - type Liste

On peut décrire un type "liste d'entiers" par le type abstrait suivant :


type Liste
{ constructeurs }
ListeVide

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (7 of 11) [23-09-2001 13:40:46]


Structures de données

Cons : entier * Liste -> Liste


{ opérations }
Longueur : Liste -> entier
Tete : Liste -> entier
Fin : Liste -> Liste
Concat : Liste * Liste -> Liste
...

Une façon de représenter ce type abstrait par un type concret est d'utiliser un tableau qui va contenir les éléments de la
liste :
type Liste = produit
longueur : entier
contenu : tableau [1..max] de entier
fin

fonction ListeVide : Liste


lexique
l : Liste
début
l.longueur := 0
retourner l
fin

fonction Cons (x : entier, l : Liste) : Liste


lexique
i : entier
res : Liste
début
{ il faudrait verifier que longueur <= max }
res.longueur := l.longueur + 1
pour i parcourant [1..l.longueur]
res.contenu [i+1] <- l.contenu [i]
retourner res
fin

...

Pour des raisons d'efficacité, on peut souhaiter que Cons ajoute l'élément dans la liste passée en argument plutôt que de
retourner une nouvelle liste. Il serait alors judicieux de mettre les éléments dans la liste dans l'ordre inverse. Ainsi un
ajout en tête de liste revient a ajouter l'élément a la fin du tableau :
procédure Cons (x : entier, donnée-résultat l : Liste)
début
{ il faudrait verifier que longueur <= max }
l.longueur := l.longueur + 1
l.contenu [l.longueur] <- x
fin

Exemple 2 - type Graphe

On veut représenter les relations entre un ensemble de gares SNCF. On se donne un type énuméré représentant les gares :

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (8 of 11) [23-09-2001 13:40:46]


Structures de données

type gares = (Lille, Paris, Lyon, Nancy,


Marseille, Bordeaux, Nice)

Considérons les liaisons suivantes :

On peut aussi les représenter dans un tableau à deux dimensions :


Lille Paris Lyon Nancy Marseill Bordeaux Nice
e
Lille X X
Paris X X X X X
Lyon X X X X
Nancy X X
Marseille X X X
Bordeaux X
Nice X

Ce tableau peut être représenter par un type tableau :


type ligne_SNCF = tableau [gares, gares] de booléen

Un tel graphe de connexions peut être utilisé pour répondre a un certain nombre de questions :
● Peut-on se rendre de n'importe quelle gare à n'importe quelle autre gare ? (Le graphe est-il connexe ?)

● Etant données deux gares A et B combien y a-t-il au minimum de changements ? (Plus cours chemin).

● etc.

On peut aussi remplacer les croix par des distances kilométriques, et calculer ainsi la distance entre deux gares. On peut
également avoir des liaisons à sens unique (la matrice n'est alors plus symétrique).
Types somme

Le type somme permet de représenter des unions disjointes de domaines.


Par exemple, les solutions d'une équation du second degré peuvent être :
● 2 racines réelles ;

● 2 racines complexes ;

● une racine double ;

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (9 of 11) [23-09-2001 13:40:46]


Structures de données

On peut définir le type abstrait suivant pour représenter ces solutions :


type solutions
{constructeurs}
réelles : réel * réel -> solutions
complexes : réel * réel -> solutions
double : réel -> solution

{opérations}
type_solution -> réelles | complexes | double

{invariants}
tag (réelles (r1,r2)) = réelles
tag (complexes (c1, c2)) = complexe
tag (double (d)) = double

On note un type somme de la façon suivante :


type type_solution = (réelles, complexes, double)
type solutions = somme selon t : type_solution
réelles : r1, r2 : réel
complexes : c1, c2 : réel
double : d : réel
fin

Le type somme se comporte comme un type enregistrement : t, r1, r2, c1, c2, r sont des champs accessibles par la notation
pointée. Le champ t est appelé "tag" ou "discriminant" car il indique dans quel sous-domaine on se trouve.
lexique
racines : solutions
début
...
selon le cas d
d > 0 :
racines.t <- réelles
racines.r1 <- ...
racines.r2 <- ...
d = 0 :
racines.t <- double
racines.d <- ...
d < 0 :
racines.t <- complexes
racines.c1.re <- ...
racines.c1.im <- ...
racines.c2.re <- ...
racines.c2.im <- ...

Il faut noter que l'accès à un champ n'est légal que si la valeur du discriminant correspond au sous-domaine qui contient ce
champ. Ainsi, dans l'exemple ci-dessus, racines.d est illégal si racines.t vaut réelles.

Autres chapitres :
1. Introduction - Langage de réalisation

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (10 of 11) [23-09-2001 13:40:46]


Structures de données

2. Structures de données (ce chapitre)


3. Lexique et syntaxe
4. Sémantique

Michel Beaudouin-Lafon, mbl@lri.fr

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/2-structures_donnees.html (11 of 11) [23-09-2001 13:40:46]


Lexique et syntaxe

DEUG MIAS M2

Lexique et syntaxe

1. Introduction
1. Comment décrire un langage de programmation
2. Lexique
1. Identificateurs et Mots-clés
2. Constantes
3. Symboles spéciaux
4. Commentaires
3. Syntaxe
1. Déclarations
2. Accès aux variables
3. Expressions
1. Opérateurs arithmétiques
2. Opérateurs relationnels
3. Opérateurs logiques
4. Priorités
4. Instructions

Introduction

Les deux chapitres précédents ont présenté les principes de base de la programmation impérative, à
savoir les notions de :
● actions ;

● variables et affectation ;

● types abstraits et concrets (énumération, tableaux, produits, sommes).

Le but de cette seconde partie du cours est double :

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (1 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

● donner les éléments nécessaires à l'apprentissage d'un nouveau langage de programmation


impérative (ou éventuellement autre) à partir de son manuel de référence ;
● expliquer plus en détail certains aspects de la programmation impérative comme les règles de
portée, l'affectation, le passage de paramètre, les pointeurs, etc.
Pour cela, nous allons expliquer comment décrire avec précision un langage de programmation.
Comment décrire un langage de programmation

Lorsque l'on apprend une langue (par exemple l'anglais), on doit maîtriser :
● le vocabulaire : mots du dictionnaire, déclinaisons, conjugaisons, etc. ;

● la grammaire : comment composer des phrases correctes du point de vue de leur forme et de leur
structure sinon de leur sens. Par exemple, "le chat mange la maison" est correct du point de vue
grammatical.
● le sens des mots et des phrases. Généralement, cet apprentissage se fait en même temps que le
vocabulaire et la grammaire : on apprend les mots corrects et leur sens, et les constructions
grammaticales et leur sens. Cependant, l'exemple ci-dessus montre que l'on peut séparer la
correction grammaticale de la correction du sens (dite correction sémantique).
Un langage de programmation (que l'on peut considérer comme une langue artificielle par rapport aux
langues naturelles comme le français et l'anglais) peut être décrit par les mêmes 3 niveaux :
● le vocabulaire, appelé lexique ;

● la grammaire, appelée syntaxe ;

● le sens, appelé sémantique.

La différence principale entre un langage de programmation et une langue naturelle est qu'un langage de
programmation est beaucoup plus simple et beaucoup plus strict qu'une langue naturelle.
Une bonne façon de mettre en évidence les trois niveaux ci-dessus est de prendre des exemples de
(morceaux de) programmes erronés (ici en Pascal) :
1 program p;
2 var 12x, y : boolean;
3 t : array [1..10] of integer;
4 begin
5 if y <> 0
6 else t[11] := 0;
7 end.

La ligne 2 contient une erreur lexicale : 12x n'est pas un identificateur légal en Pascal. La ligne 5/6
contient une erreur syntaxique : la construction if...else n'est pas légale en Pascal (il faut une partie then).
La ligne 5 contient également une erreur sémantique : la variable y est déclarée booléenne, on ne peut
donc la comparer à l'entier 0. Enfin la ligne 6 contient une autre erreur sémantique : 11 n'est pas un indice
valide pour le tableau t. Ces deux erreurs sémantiques sont de nature différente : la première peut être
détectée simplement en regardant le texte du programme, on dit qu'il s'agit d'une erreur de sémantique
statique. La seconde ne peut être détectée (dans le cas général) que lorsque le programme s'exécute, on

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (2 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

dit qu'il s'agit d'une erreur de sémantique dynamique. Nous reviendrons sur ces notions en temps utile.
Les trois niveaux lexique/syntaxe/sémantique sont également à la base de la conception des compilateurs.
Un compilateur est un programme qui traduit un programme écrit dans un langage évolué (par exemple
Pascal) en code machine exécutable directement sur la machine. C'est grâce à un compilateur que l'on
peut écrire et exécuter des programmes dans des langages évolués. La structure générale d'un
compilateur consiste à découper le programme qu'il traduit en mots (unités lexicales), a structurer ces
mots en arbres syntaxiques, puis à analyser cet arbre pour vérifier qu'il est sémantiquement correct.
Ensuite seulement, la traduction proprement dite peut commencer : chaque morceau de l'arbre est traduit
en langage machine équivalent, puis ce langage machine est éventuellement optimisé. Inutile de dire
qu'un compilateur est un programme particulièrement complexe ! Ces concepts sont approfondis dans le
cours de M3 Spécialité Informatique.
Dans la suite de ce chapitre, nous nous intéressons aux éléments usuels des lexiques des langages de
programmation impératifs et à la description de la syntaxe des langages.
Lexique

Les unités lexicales d'un langage décrivent ses composants de base, les "mots" de son vocabulaires. La
plupart des langages sont fondés sur des vocabulaires très proches. Nous allons ici décrire les principales
catégories d'unités lexicales et détailler le cas du langage Pascal.
Identificateurs et Mots-clés

Les "mots" les plus évidents dans un programme sont les identificateurs et les mots-clés. Les
identificateurs sont utilisés pour dénoter des "objets" du programme : variables, fonctions, types, etc. Les
mots-clés sont utilisés pour faciliter la reconnaissance de la structure du programme. Par exemple en
Pascal, les mots-clés begin et end délimitent des blocs d'instructions.
En général (en en particulier en Pascal), les identificateurs sont constitués d'au moins une lettre, suivie de
lettres ou de chiffres. Certains caractères (comme le souligné "_") sont parfois également autorisés. Le
nombre de caractères qui sont effectivement utilisés pour différencier deux identificateurs dépend du
langage : 8 dans la version originale de Pascal. Ainsi
12x est illégal mais x12 est légal
toto_tata_tutu et toto_tata_titi sont identiques

Les mots-clés sont des mots particuliers qui ne peuvent pas être utilisés comme identificateurs. En
Pascal, la liste des mots-clés est la suivante :
program const var type function procedure begin end
array record set of file
if then else while do repeat until case for to downto
and or not in div mod
goto label nil packed with
Constantes

Les constants sont de mots qui désignent des valeurs d'un type prédéfini du langage. Typiquement, on

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (3 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

trouve les constantes entières, réelles, booléennes, caractères et éventuellement chaînes. Ce qui suit
concerne le langage Pascal :
Les entiers et les réels s'écrivent sous la forme mathématique habituelle :
10 -50 32767 153648293764234 (illégal car trop grand)
3.14 -0.05 6.02e+23 1.0e-6 1.0 (différent de 1)

Les constants booléennes sont les deux mots


true et false

Les constantes caractères sont notées entre simple quotes. Le caractère simple quote est noté en répétant
la quote :
'a' '9' ';' ''''

Les constantes chaînes de caractères sont également notées entre simple quotes, avec répétition des
quotes éventuelles :
'Bonjour' 'Commet allez-vous aujourd''hui'

Dans d'autres langages, par exemple C, on utilise des doubles quotes pour les chaînes de caractères, et le
caractère '\' ("backslash") pour "protéger" les doubles quotes et indiquer des caractères spéciaux :
"Il dit : \"bonjour !\""
"Un deux\ntrois quatre" "\n" indique un retour à la ligne
"voici un backslash : \\"
Symboles spéciaux

Les langages de programmation utilisent de nombreux symboles spéciaux no alphanumériques. Par


exemple, en Pascal il y en a 24 :
+ - * / := . , ; : ' = <> < <= >= > ( ) [ ] { } ^ ..

Certains de ces caractères sont des opérateurs artihmétiques et relationnels :


+ - * / = <> < <= > >=

D'autres correspondent à des opérateurs particuliers :


:= affectation
[ ] accès à un tableau
^ déréférencer un pointeur

D'autres encore sont utilisés pour séparer des instructions ou grouper des expressions :

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (4 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

, séparer les éléments d'une liste


; séparer des instructions
( ) grouper des sous-expressions

L'espace sert de séparateur, c'est-à-dire qu'il permet de séparer des unités lexicales adjacentes. Par
exemple
typet=array[1..10]ofinteger

est incorrect et doit s'écrire avec au moins deux espaces :


type t=array[1..10]of integer
type t = array [1 .. 10] of integer

En Pascal, les fins de ligne et les tabulations sont également des séparateurs.
Commentaires

Les commentaires sont des parties du programme qui sont ignorées du compilateur et qui ne changent
donc rien au sens du programme. Ils sont cependant très importants pour documenter le programme,
notamment en vue de son utilisation par quelqu'un d'autre ou de sa modification ultérieure (il est très rare
d'écrire un programme que l'on ne modifie plus jamais et que l'on ne réutilise pas par ailleurs).
En Pascal, les deux formes suivantes sont acceptées :
(* un commentaire *)
{ un autre commentaire }
Syntaxe

La syntaxe d'un langage décrit les façons correctes d'assembler des unités lexicales pour écrire des
programmes. Un programme syntaxiquement correct peut encore être erroné s'il contient des erreurs
sémantiques.
Pour décrire la grammaire d'une langue naturelle, on utilise parfois des schémas (et le plus souvent des
règles). Par exemple, un phrase en français peut être composée d'un groupe sujet, d'un groupe verbal,
d'un groupe complément et d'un point final. Le groupe sujet est constitué d'un article, d'un adjectif
éventuel et d'un nom ou d'un nom et d'un adjectif. Le groupe verbal est composé d'un auxiliaire éventuel
et d'un verbe. Le groupe complément est composé d'un préposition éventuelle et d'un groupe sujet. Par
exemple, les phrases suivantes correspondent à cette description :
Le chat mange la souris. Le cadavre exquis boira le vin nouveau. Le partiel aura lieu dans le
grand amphi.
La figure ci-dessous explicite la structure syntaxique de la première phrase d'après les règles de
grammaire expliquées ci-dessus :

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (5 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

Cependant la description ci-dessus est fastidieuse à lire et peut être difficile à comprendre. Une notation
un peu plus formelle peut faciliter la lecture et la compréhension. Il suffit pour cela de donner un nom à
chaque noeud possible de l'arbre et de décrire quels sont les fils possibles de chaque noeud :
<phrase> -> <g_sujet> <g_verbe> <g_complément> .
<g_sujet> -> article [ adjectif ] nom
| article nom adjectif
<g_verbe> -> [ auxiliaire ] verbe
<g_complément> -> [ préposition ] <g_sujet>

Cette notation correspond à la description d'une grammaire de production. Chaque ligne correspond à
une règle de production. Les noms entre chevrons (comme <phrase>) sont appelés non-terminaux. Les
autres sont les terminaux, c'est-à-dire des mots du vocabulaire. Pour construire une phrase
grammaticalement correcte, il faut construire un arbre dont la racine est le non-terminal à gauche de la
première règles et dont les fils sont les terminaux et non-terminaux de la partie droite. Chaque feuille de
l'arbre qui est un non-terminal doit être complétée en utilisant une règle dont le non-terminal est en partie
gauche. Il faut noter que dans cet exemple, on peut produire des phrases "insensées". Dans les règles de
production ci-dessus, les crochets permettent de mentionner des parties optionnelles et les barres
verticales des alternatives.
Cette notation formelle n'est pas toujours facile à lire. On peut utiliser une autre représentation des règles
de production, les diagrammes de Conway ou diagrammes syntaxiques :

Souvent, on trouve dans les grammaires des structures répétitives. Par exemple, un texte est une suite de

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (6 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

phrases. Ces structures se traduisent par des règles récursives dans les grammaires de production, mais
on peut souvent les représenter par des boucles dans les diagrammes syntaxiques :
<texte> -> <phrase>
| <phrase> <texte>

Les langages de programmation ont une structure à la fois plus simple et plus stricte que les langues
naturelles. Ils se prêtent donc mieux à la représentation par règles de production et diagrammes
syntaxiques. Les principales structures syntaxiques que l'on trouve dans les langages impératifs sont les
suivantes :
● les déclarations (de constantes, types, variables, fonctions, procédures, paramètres, etc.) ;

● les accès aux variables (notation pointée, accès à un tableau, etc.).

● les expressions (arithmétiques ou logiques) ;

● les instructions (affectation, conditionnelles, boucles, etc.) ;

Dans la suite, nous présentons ces principales structures syntaxiques en utilisant tantôt des règles de
production, tantôt des diagrammes syntaxiques, et en étudiants particulièrement le cas de Pascal. La
plupart des manuels de référence utilisent des règles de production. Les diagrammes syntaxiques sont
plus rares.
Déclarations

Un programme impératif est généralement constitué d'un ensemble de déclarations et d'un ensemble
d'instructions. Pour un programme Pascal, on a :

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (7 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

Les déclarations de constantes, types, variables, fonctions et procédures sont toutes optionnelles. Elles
font référence aux déclarations de type décrites ci-dessous :

Un type simple correspond aux types prédéfinis (entier, réel, booléen, caractère), aux types énumérés et

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (8 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

aux types intervalles. Les types composés sont les tableaux et les enregistrements, ainsi que d'autres
types que nous n'avons pas étudié (pointeurs, ensembles, fichiers).
Le détail de la déclaration des champs d'un enregistrement (non-terminal champs) n'est pas donné et est
laissé à titre d'exercice.
Accès aux variables

Pour accéder à une variable dans un programme, il suffit de mentionner son identificateur. Cependant, les
types composés permettent de définir des variables complexes : ainsi, un tableau contient plusieurs
éléments, et un enregistrement contient des champs. Il est possible de combiner ces types, et d'avoir ainsi
un tableau d'enregistrements ou un enregistrement dont certains sont des tableaux, etc. La syntaxe de
l'accès aux variables permet de décrire comment dénoter les éléments de tableaux (t [i]) et les champs
d'enregistrements (c.im) et comment combiner ces notations. En Pascal, on a la syntaxe (incomplète)
suivante :

Par exemple, la déclaration


var m = array [1..10, 1..10] of record re, im : real end;

définit m comme un tableau à deux dimensions dont les éléments sont des nombres complexes. Pour
accéder à la partie réelle de l'élément d'indices 2, 3, il faut écrire :
m [2, 3].re
Expressions

Une expression dénote une valeur. Les expressions sont construites à partir d'un ensemble d'opérateurs.
On distingue les opérateurs arithmétiques, relationnels et logiques. La notation est proche de celle utilisée
en mathématiques (attention cependant : le produit doit être explicitement noté *).

Opérateurs arithmétiques

Ce sont les 4 opérations usuelles : +, -, *, /. En Pascal, on a également les opérateurs DIV et MOD pour
les nombres entiers, qui représentent le quotient et le reste de la division entière. Ainsi, si b != 0 :
a = (a DIV b) * b + a MOD b.
Les opérateurs * et / ont une plus grande priorité que les opérateurs + et -. Ainsi

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (9 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

a + b * c se lit a + (b * c).
En cas d'opérateurs successifs de même priorité, l'associativité est à gauche :
a - b + c est évalué (a - b) + c.
Si nécessaire, on utilise des parenthèses pour grouper des sous-expressions :
(a + b) * c est différent de a + b * c.
Enfin, l'opérateur unaire - est de priorité supérieure à tous les autres :
- a + b se lit (- a) + b.

Opérateurs relationnels

Les opérateurs relationnels permettent de comparer des valeurs d'un type simple. Ils ont pour signature :
entier x entier -> booléen réel x réel -> booléen
En Pascal, les opérateurs sont :
= <> < <= > >=

Ils sont tous de même priorité. En effet, on ne peut avoir une expression de la forme :
a<b<c
Celle-ci serait évaluée de la façon suivante, qui provoque une erreur car le terme entre parenthèse est de
type booléen mais pas le terme de droite :
(a < b) < c

Opérateurs logiques

Les opérateurs logiques portent sur les booléens. Ce sont le ou logique (OR), le et logique (AND) et la
négation logique (NOT). Ils ont pour priorités (par ordre croissant) : OR AND NOT.

Priorités

Les opérateurs arithmétiques sont de priorité supérieure aux opérateurs relationnels, eux-même de
priorité supérieur aux opérateurs logiques. Ainsi, l'expression
x + y > 10 and x - y < 0
est évaluée dans l'ordre suivant :
((x + y) > 10 ) and ((x - y) < 0)
Les priorités sont donc, par ordre croissant :
● OR

● AND

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (10 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

● NOT
● = <> < <= > >=
● +-
● */
● - unaire
Les facteurs qui constituent les opérandes d'une expression peuvent être des constantes, des variables, ou
des appels de fonction. Ainsi, si l'on ne tient pas compte des priorités, la syntaxe des expression peut se
décrire comme suit :

Pour prendre en compte les priorités des opérateurs, il faut décomposer expression en plusieurs niveaux,
et donner autant de diagrammes correspondants (à faire en exercice).
Instructions

Les instructions constituent la partie action d'un programme impératif. Dans un langage impératif, on
trouve au moins les instructions suivantes :
● affectation et appel de procédure

● conditionnelles (if et case en Pascal)

● boucles (while, repeat et for en Pascal).

De plus, des instructions peuvent être groupées dans un bloc (begin-end en Pascal). Le diagramme
syntaxe des instructions Pascal est le suivant :

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (11 of 12) [23-09-2001 13:41:34]


Lexique et syntaxe

Les instructions case, repeat et for ne sont pas complétées dans ce diagrammes. Elles sont laissées à titre
d'exercice.

Autres chapitres :
1. Introduction - Langage de réalisation
2. Structures de données
3. Lexique et syntaxe (ce chapitre)
4. Sémantique

Michel Beaudouin-Lafon, mbl@lri.fr

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/3-lexique-syntaxe.html (12 of 12) [23-09-2001 13:41:34]


Sémantique

DEUG MIAS M2

Sémantique

1. Introduction
1. Sémantique statique
2. Sémantique dynamique
2. Environnement et mémoire
1. Mémoire
2. Environnements
3. Déclarations et instructions
3. Portée des déclarations
1. Portée statique et portée dynamique
4. Sémantique de l'affectation
1. Emplacement d'une variable
2. Valeur d'une expression
1. Effets de bord
5. Sémantique des autres instructions
1. Séquence
2. Conditionnelle
3. Boucles
6. Procédures et fonctions
1. Passage par valeur
2. Passage par variable
1. Usage du passage par variable
3. Appels récursifs
7. Les pointeurs
8. Contrôle de types

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (1 of 15) [23-09-2001 13:42:24]


Sémantique

Introduction

La sémantique d'un langage de programmation est la description du sens des constructions du langage.
Comme dans une langue naturelle, les phrases syntaxiquement correctes d'un langage de programmation
n'ont pas toutes un sens. Le compilateur, qui a la charge de traduire un programme source (par exemple
en Pascal) vers un programme équivalent en langage machine, doit donc s'assurer que le programme a
bien un sens (on dit qu'il est correct sémantiquement), avant d'effectuer la traduction à proprement parler.
Dans cette partie du cours, nous nous intéressons aux mécanismes qui permettent de décrire la
sémantique d'un langage de programmation impératif. Ces mécanismes sont appliqués au langage Pascal
dont une partie de la sémantique sera décrite. Les notions abordées sont notamment :
● environnement et mémoire

● portée des déclarations

● appel de procédures et fonctions

● les pointeurs

● typage

Sémantique statique

Dans le programme suivant, qui est correct syntaxiquement, on peut détecter aisément plusieurs erreurs
sémantiques :
1 program demo;
2 var x : integer;
3 t : array [1..10] of integer;
4 procedure p (z : real);
5 var i : integer;
6 begin
7 z := i;
8 end; (* p *)
9 begin (* program demo *)
10 readln (i);
11 x := t;
12 p (x, i);
13 end.

Dans la procédure p, la variable i est utilisée (ligne 7) mais n'est pas initialisée. A la ligne 10, on utilise la
variable i alors qu'elle n'est pas déclarée dans le bloc de déclarations correspondant. A la ligne 11, on
affecte une variable t qui est un tableau à une variable x entière. Enfin à la ligne 12, la procédure p est
appelée avec deux paramètres (x et i) alors qu'elle n'a été déclarée qu'avec un paramètre.
On voit que ces erreurs font référence à la notion de cohérence entre la déclaration d'un objet et ses
utilisations. Dans un langage impératif, on impose en général de déclarer les objets (variables, types,
procédures, etc.) avant de les utiliser, afin de pouvoir vérifier que l'utilisation est conforme à la

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (2 of 15) [23-09-2001 13:42:24]


Sémantique

déclaration.
Une erreur comme celle de la ligne 10 a trait à ce que l'on appelle la portée des identificateurs : lorsque
l'on déclare un identificateur, cette déclaration n'est utilisable que dans une certaine partie du programme.
Ici, la variable i, bien que déclarée dans p, n'est pas utilisable dans le programme principal.
Une erreur comme celle de la ligne 11 a trait au contrôle des types : on vérifie que chaque utilisation
d'une variable correspond bien au type qu'on a déclaré pour cette variable.
Enfin, une erreur comme celle de la ligne 12 a trait à la sémantique de l'appel de procédures et de
fonctions.
Dans la suite de ce cours, ces trois types de contrôles sémantiques seront étudiés en détail. L'ensemble de
ces contrôles correspondent à la vérification de la sémantique statique du programme, c'est-à-dire de la
partie de la sémantique qui peut être vérifiée sans exécuter le programme. La sémantique statique est
contrôlée par le compilateur, qui interdit l'exécution du programme si elle n'est pas correcte.
Sémantique dynamique

Cependant, il n'est pas possible en général d'assurer que tout ce que fait le programme a un sens. En
d'autres termes, toute la sémantique d'un langage ne peut être décrite pas sa sémantique statique (ou alors
le langage est trop simple et sans intérêt). Considérons le programme suivant :
program p;
var t : array [1..10] of integer;
i : integer;
function f (n : integer) : integer;
begin
if n > 2
then f := f (n-1) + f (n-2)
else f := n
end; (* f *)

begin (* program p *)
i := 1;
while f(i) < 1000 do
begin
writeln (t[i]);
i := i + 1;
end
end.

L'expression t[i] n'a de sens que si i est dans l'intervalle [1..10]. Ici, i vaut 1 au début, et est incrémenté
jusqu'à ce que f(i) soit supérieur ou égal à 1000. Malheureusement il est impossible de savoir si la valeur
de f(i) sera inférieure à 1000 lorsque i est inférieur à 10, même en regardant le corps de la fonction f, à
moins de calculer effectivement les valeurs de f(i), c'est-à-dire d'exécuter le programme.
On appelle cette partie de la sémantique la sémantique dynamique car elle ne peut être contrôlée que lors

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (3 of 15) [23-09-2001 13:42:24]


Sémantique

de l'exécution du programme. L'un des avantages des langages impératifs est de diminuer la sémantique
dynamique par rapport à la sémantique statique. Cela a pour effet qu'un plus grand nombre d'erreurs
sémantiques sont découvertes avant d'exécuter le programme, à la compilation, et que les erreur
d'exécution, qui sont difficiles à découvrir et à réparer, sont plus rares.
Environnement et mémoire

Une façon de décrire ce que fait un programme est de décrire ses effets observables lors de l'exécution. A
l'exécution, un programme exécute des instructions (actions élémentaires ou composées) qui modifient :
● les valeurs des registres de l'unité centrale ;

● les valeurs stockées en mémoire.

Nous nous intéressons particulièrement aux effets d'un programme sur la mémoire. On considère que la
mémoire est constituée d'emplacements, qui peuvent contenir des valeurs. Ainsi, un emplacement de 4
octets pourra contenir un entier, tandis qu'un emplacement de 200 octets pourra contenir un tableau de
200 caractères. Un emplacement correspond à une adresse dans la mémoire et une taille. On appelle E
l'ensemble des emplacements et V l'ensemble des valeurs.
Mémoire

A un instant donné, l'état de la mémoire peut être vu comme une fonction qui donne la valeur contenue
chacun de ses emplacements :
mem : E -> V
L'ensemble de tous les états possibles de la mémoire est l'ensemble de toutes les fonctions mem, noté
MEM. A un instant donné, l'état de la mémoire est donc un élément de MEM.
Lorsque l'on change une valeur en mémoire, l'état de la mémoire change et correspond donc à un autre
élément de MEM. Ainsi, si la valeur de l'emplacement e devient v, mem devient mem + {e -> v}. Cette
notation signifie que, dorénavant, mem(e) = v. Au départ, on peut considérer que la mémoire est vide
(mem(e) est indéfini quel que soit e), on bien qu'elle est initialisée avec des valeurs par défaut (par
exemple mem(e) = 0 quel que soit e).
Cette modification de la mémoire peut être décrite par une fonction :
changevaleur : MEM x E x V -> MEM (mem, e, v) -> mem + {e->v}
Environnements

Dans un programme écrit dans un langage impératif, les emplacements sont dénotés par des
identificateurs de variables. On appelle ID l'ensemble de tous les identificateurs possibles. A tout instant,
on peut décrire l'emplacement dénoté par chaque identificateur du programme par une fonction appelée
environnement :
env : ID -> E
Ainsi, pour connaître la valeur de la variable i dans un programme, il faut connaître l'emplacement e de i,
et la valeur v de e :

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (4 of 15) [23-09-2001 13:42:24]


Sémantique

e = env (i), v = mem (e), soit v = mem (env (i))


Au départ, env(i) est indéfini quel que soit i, c'est-à-dire qu'aucun identificateur n'est défini. Lorsque l'on
définit un identificateur, il faut lui associer un emplacement, c'est-à-dire qu'il faut changer la fonction
env. Comme pour le changement de valeur en mémoire, ce changement peut être décrit par une fonction :
declare : ENV x ID x E -> ENV (env, id, e) -> env + {id -> e}
Pour faire une analogie, une variable peut être vue comme le nom que l'on donne à une boîte, par
exemple "mon bloc-tiroir" ou "l'armoire de l'entrée", et la valeur est le contenu de la boîte. Pour des
personnes différentes, ou selon l'endroit où je me trouve, le nom "mon bloc-tiroir" ou "l'armoire de
l'entrée" dénote un objet différent : cela correspond à des environnements différents.
Déclarations et instructions

Dans un langage impératif, l'environnement est modifié seulement par les déclarations, et la mémoire est
modifiée seulement par les instructions.
Considérons le programme suivant :
program démo;
var x : integer;
begin
x := 5;
end.

On trouve une déclaration (var x) et une instruction (x := 5). Au départ, l'environnement et la mémoire
sont vides. Notons-les respectivement env0 et mem0.
L'effet de la déclaration est d'associer un emplacement libre (par exemple 1) à la variable x.
L'environnement devient donc
env = env0 + {x -> 1}
L'effet de l'instruction est d'affecter une valeur (5) à l'emplacement env(x). La mémoire devient donc
mem = mem0 + {env(x) -> 5}
Il apparaît donc que l'instruction x := 5 n'aurait pas de sens si la déclaration de x était omise, car env (x)
ne serait pas défini. Ainsi, en Pascal, comme dans tous les langages impératifs, toute variable doit être
déclarée avant d'être utilisée. Cela est également vrai des autres entités du langage : constantes, types,
procédures.
Considérons maintenant le programme suivant :
program démo;
var x, y : integer;
begin
x := y;
end.

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (5 of 15) [23-09-2001 13:42:24]


Sémantique

Avant l'affectation, l'environnement est


env = env0 + {x -> 1} + {y -> 2}
(on suppose que les emplacements des entiers ont une taille de 1). Pour réaliser l'affectation, il faut
connaitre l'emplacement de x, et la valeur de y, soit respectivement env(x) et val(env(y)). L'effet de
l'affectation est de modifier la mémoire de la façon suivante :
mem = mem0 + {env(x) -> mem(env(y)) }
Comme x et y ont été déclarés, env(x) et env(y) sont définis. Par contre, mem0 est indéfinie pour tout
emplacement et donc mem(env(y)) est indéfini : la valeur de y n'a pas été initialisée, et l'affectation est
donc sémantiquement incorrecte.
Considérons donc le programme suivant :
1 program démo;
2 var x, y : integer;
3 begin
4 y := 5
5 x := y;
6 end.

Avant d'exécuter la ligne 5, l'état de la mémoire est


mem = mem0 + {env(y) -> 5 }
La ligne 5 produit la modification de mem en
mem + {env(x) -> mem(env(y)) }
Cette fois-ci, mem(env(y)) est bien défini et vaut 5.
On voit donc que les notions d'environnement et de mémoire permettent déjà de réaliser deux contrôles
sémantiques :
● variables non déclarées ;

● variables non initialisées.

Note : Dans un langage fonctionnel, il n'y a pas besoin de la notion de mémoire pour définir la
sémantique du langage. En effet, un environnement fait correspondre directement un identificateur à une
valeur.
Portée des déclarations

Nous avons vu qu'une déclaration modifiait l'environnement en définissant un emplacement pour la


variable déclarée. Dans la plupart des langages impératifs, les variables ont une portée, qui détermine la
portion du programme où la variable est accessible (on dit : visible). Ainsi, dans un programme Pascal,
les déclarations globales sont visibles dans tout le programme (d'où leur nom). Par contre, les
déclarations de variables locales à une procédure ou fonction ont pour portée la seule procédure ou

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (6 of 15) [23-09-2001 13:42:24]


Sémantique

fonction où elles sont déclarées, ainsi que les procédures et fonctions imbriquées :
program demo
var x : integer;
procedure p;
var y : integer;
begin
... x ... y ...
end;
begin
... x ...
end.

On peut représenter, dans la plupart des cas, les portées comme des boîtes imbriquées : les variables
déclarées dans une boîte sont visibles dans cette boîte et dans les boîtes imbriquées. (On peut également
représenter les portées comme un arbre : les variables déclarées à un noeud de l'arbre sont visibles dans
ce noeud et dans tous les noeuds de son sous-arbre.) Lorsque l'on atteint la fin d'une portée,
l'environnement est restauré à son état juste avant le début de la portée.

En 1, les déclarations visibles sont celles de Prog, P et P2. En 2, celles de Prog et de Q. En


3, celles de Prog.
Une variable d'un bloc englobant peut être cachée par la déclaration d'une variable de même nom dans le
bloc courant :
program demo
var i : integer;
procedure p;
var i : integer;
begin
... i ...
end;

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (7 of 15) [23-09-2001 13:42:24]


Sémantique

begin
... i ...
end.

Cela correspond au fait que lorsque l'on déclare i dans p, ou modifie l'environnement pour y ajouter
{i->env'(i)}, écrasant l'ancienne valeur env(i). Cependant, lorsque l'on est en dehors de la portée de p,
c'est la première définition de i qui est visible (celle du programme principal). On voit donc que, selon le
point du programme où l'on se trouve, un même identificateur peut désigner un emplacement différent.
Pour calculer l'environnement en un point du programme, on peut considérer qu'à chaque portée P
correspond un environnement EP. L'environnement courant est la "somme" des environnements associés
à chaque portée contenant la position courante. La "somme" de deux environnements est définie comme
suit :
E0 = E1 + E2 : ID ->E pour tout x tel que E2(x) = y, E0(x) = y pour tout x tel que E1(x) = y
et E2(x) n'est pas défini, E0(x) = y
Cette définition n'est pas tout à fait exacte car un identificateur n'est visible qu'à partir de son point de
déclaration. En particulier, si l'on déclare la procédure P puis la procédure Q, P est visible depuis le corps
de Q mais pas l'inverse.
En Pascal, une portée correspond au programme principal, et à chaque procédure ou fonction. Le nom de
la procédure ou fonction fait cependant partie de l'environnement englobant - sinon, il serait impossible
de l'appeler !

Portée statique et portée dynamique

Ce que nous avons décrit jusqu'à présent s'appelle la portée statique. Cela signifie que l'emplacement
associé à une variable qui apparait dans le programme dépend de la déclaration de cette variable.
Sémantique de l'affectation

L'affectation est une instruction de la forme


var := expr
"var" est une variable simple ou composée (par exemple l.t[i].x), et "expr" est une expression. La
sémantique d'une affectation simple est facile à décrire :
x := 5
change la mémoire de telle sorte que l'emplacement de x contienne 5. Lorsque la variable n'est pas une
variable simple, il faut être capable de définir l'emplacement e représenté par la variable. Lorsque
l'expression n'est pas une simple constante, il faut être capable de définir la valeur v représentée par cette

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (8 of 15) [23-09-2001 13:42:24]


Sémantique

expression. Un fois connus l'emplacement e et la valeur v, la sémantique de l'affectation peut être


simplement définie par :
mem = mem0 + {e -> v}
c'est-à-dire que la mémoire mem0 est modifiée de telle sorte que la valeur de l'emplacement e est v.
Emplacement d'une variable

Soit un environnement env et une mémoire mem. Pour déterminer l'emplacement d'une variable var, il
faut traiter chaque cas :
● var est une variable simple x. L'emplacement est alors env(x).

● var est un élément de tableau t[i]. Les éléments d'un tableau sont stockés de façon contigue en
mémoire, et chaque élément occupe la même taille te. L'emplacement de t[i] a pour adresse
adresse(t) + i * te et pour taille te. Pour une variable de la forme t[expr] où expr est une expression
quelconque, l'emplacement a pour adresse adresse(t) + val(expr) * te. val(expr) est la valeur de
expr et est décrit plus loin.
● var est un champ de structure c.x. Les champs d'une structure sont stockés de façon contigue en
mémoire, chaque champ ci ayant un taille ti. On peut associer à chaque champ ci un déplacement
égale à la somme de cj pour j < i. L'emplacement de c.x a alors pour adresse adresse(c) + depl(x) et
pour taille taille(x).
Nous savons donc calculer l'emplacement de toute variable complexe, en combinant les règles ci-dessus.
Nous noterons cet emplacement Empl[[var]]. (Cette notation permet de distinguer la fonction env,
définie sur l'ensemble des identificateurs, de l'évaluation de l'emplacement d'une variable, qui n'est pas à
proprement parler une fonction). Les règles ci-dessus peuvent alors s'écrire :
● Empl[[ x ]] = env(x)

● Empl[[ t[expr] ]] = T (env(t), Val[[ expr ]])

● Empl[[ c.x ]] = D (env(c), x)

(Val[[ expr ]] est décrit ci-dessous)


Valeur d'une expression

Nous considérons à nouveau un environnement env et une mémoire mem. Pour déterminer la valeur
d'une expression, il faut, comme pour les variables, considérer les différents cas :
● expr est une variable var. La valeur de expr est mem(Empl[[ var ]]). Cela vaut en particulier pour
des variables simples : la valeur de l'expression y (par exemple dans x := y) est mem(Empl[[ y ]]),
soit mem(env(y)).
● expr est une expression binaire e1 op e2. La valeur de cette expression est calculé une fois que l'on
connait les valeurs de e1 et e2, en leur appliquant l'opération op. On peut écrire : val(expr) = val(e1
op e2) = val(e1) op val(e2). Cela n'est cependant valide qu'en l'absence d'effets de bord (voir
ci-dessous).
● expr est un appel de fonction f(x). Il faut décrire la sémantique e l'appel de fonction, que nous

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (9 of 15) [23-09-2001 13:42:24]


Sémantique

verrons plus loin.


De façon similaire à l'accès aux variables, on note la procédure d'évaluation d'une expression Val[[ expr
]]. Les règles ci-dessus s'écrivent alors :
● Val[[ var ]] = mem (Empl[[ var ]])

● Val[[ e1 op e2 ]] = Val[[ e1 ]] op Val[[ e2]]

● Val[[ f (x) ]] = (à définir)

On note que Val fait référence à Empl, et Empl fait référence à Val. Ces deux procédures d'évaluation se
font mutuellement référence. Cependant, jusqu'ici, ni Empl ni Val ne changent l'environnement ni la
mémoire. (Seule l'affectation change la mémoire.) Cela signifie en particulier que l'ordre des évaluations
n'a pas d'influence sur le résultat. Par exemple, évaluer Val[[e1]] puis Val[[e2]] produit le même résultat
que si l'on évalue d'abord Val[[e2]] puis Val[[e1]].

Effets de bord

La propriété ci-dessus n'est malheureusement pas toujours vraie. Considérons le programme suivant :
program bord;
var glob : integer;
x : integer;
function f (x : integer) : integer;
begin
glob := glob + 1;
f := x;
end;
begin
glob := 0;
x := f(1) + glob;
end.

Selon que f(1) est évalué avant ou après glob, la valeur de x n'est pas la même ! On dit que l'appel de f
produit un effet de bord sur la variable glob : l'évaluation de Val[[f(1)]] modifie la mémoire de telle sorte
que Val[[glob]] ne donne pas le même résultat selon que Val[[f(1)]] est évalué avant ou après. Un
problème similaire se poserait avec l'affectation :
t[glob] := f(1);

Les langages fonctionnels ne connaissent pas de problèmes d'effets de bord. Dans les langages
impératifs, ils sont inévitables : c'est au programmeur d'y prendre garde, et de les eviter au maximum.
Sémantique des autres instructions

Munis de la notion de procédure d'évaluation (comme Var[[ ]] et Empl[[ ]]), nous pouvons donner la
sémantique des autres instructions d'un langage impératif. Comme les instructions ne modifient que la
mémoire et pas l'environnement, il suffit de donner, pour chaque instruction, sont effet sur la mémoire,
grâce à la procédure Mem[[ ]] ("effet sur la mémoire").

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (10 of 15) [23-09-2001 13:42:24]


Sémantique

Séquence

Mem[[ I1; I2 ]] = Mem [[ I2 ]] o Mem [[ I1 ]]


Cela signifie que l'instruction I2 s'exécute dans le contexte de la mémoire que lui transmet l'instruction
I1.

Conditionnelle

Mem[[ if E then I1 else I2 ]] = si Val[[ E ]] alors Mem[[ I1 ]] sinon Mem [[ I2 ]]


Selon la valeur de E, c'est l'instruction I1 ou l'instruction I2 qui est exécutée. Si E modifie la mémoire, il
faut composer Mem[[I1]] et Mem[[I2]] avec Mem[[E]].

Boucles

Mem[[ while E do I ]]
On peut voir cette boucle comme la séquence
if E then I; if E then I; if E then I; ...
Cette répétition se poursuit jusqu'à ce que la mémoire ne soit plus modifiée. La sémantique ne peut être
exprimée de façon simple comme on l'a fait jusqu'à présent : il faut utiliser un opérateur spécial dit "plus
petit point fixe". On retrouve cet opérateur lorsque l'on essaie de définir la sémantique de fonctions ou
procédures récursives. (On peut d'ailleurs exprimer une boucle while sous forme d'une récursion).
Procédures et fonctions

Pour utiliser des procédures et fonctions, il faut les déclarer et les appeler. Lors de la déclaration, on
définit le nom de la procédure ou fonction, ses paramètres formels, le type de sa valeur de retour (pour
une fonction), ses déclarations locales, et son corps. Lors de l'appel, on donne la valeur des paramètres
réels.
Lors de l'appel d'une procédure ou fonction, les étapes suivantes sont exécutées :
1. Ajout à l'environnement des paramètres et variables locales, correspondant à l'entrée dans la portée
définie par la procédure ou fonction ;
2. Initialisation des valeurs des paramètres ("passage des paramètres"), c'est-à-dire modification de la
mémoire ;
3. Exécution du corps de la procédure ou fonction, dans l'environnement et la mémoire ainsi modifiés
;
4. Pour les fonctions, retour de la valeur de la fonction ;
5. Retour à l'environnement qui était en place avant l'appel : les paramètres et variables locales sont
retirées, et les variables éventuellement masquées "réapparaissent".
Il faut noter que seul l'environnement est restauré. Les effets de la procédure ou fonction sur la mémoire
persistent, ce qui est la cause des effets de bord que nous avons vu plus haut. Ces effets de bord ne
peuvent être visibles que sur les variables qui sont dans le domaine de portée de l'appelant de la fonction

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (11 of 15) [23-09-2001 13:42:24]


Sémantique

: puisque les paramètres et variables locales ne sont plus accessibles après l'appel, leur valeur en mémoire
n'a aucune importance.
Il existe deux modes principaux de passage de paramètres : par valeur et par variable. Le passage par
valeur correspond aux paramètres de données, tandis que le passage par variable correspond aux
paramètres de résultats ou de données-résultats.
Passage par valeur

Soit le programme suivant :


program pval;
var x : integer;
procedure p (a : integer);
begin
Ecrire ("a = ", a);
a := 0;
end;
begin
p (10);
x := 10;
p(x);
Ecrire ("x = ", x);
end.

Avant l'appel p(10), l'environnement contient la variable x, et la mémoire ne contient rien. Au moment de
l'appel, le paramètre formel a est ajouté à l'environnement, un emplacement libre lui est affecté et sa
valeur est initialisée au paramètre réel 10. La procédure écrit la valeur de a (10), puis affecte 0 à a. Après
le retour de l'appel de p, a est retiré de l'environnement (donc sa valeur 10 en mémoire devient
inaccessible).
Lors du second appel p(x), la mémoire contient 10 pour l'emplacement de x, mais le reste de la séquence
d'appel est exactement identique. En particulier, au retour de l'appel p(x), x vaut toujours 10.
On voit donc que lors du passage par valeur, il y a recopie de la valeur des paramètres réels dans les
emplacements des paramètres réels. Toute modification d'un paramètre formel est sans effet sur les
paramètres réels et de façon générale sur l'environnement que sera restauré au retour de l'appel. Les seuls
effets de bord sont possibles en affectant des valeurs à des variables globales, c'est-à-dire à des
emplacements accessibles après le retour de l'appel.
Notons enfin que si l'on avait appelé le paramètre formel de p x au lieu de a, le fonctionnement serait
rigoureusement le même ! Le x, paramètre formel de p, masquerait le x, variable globale, et donc
l'affectation de x dans p serait sans effet sur le x global.
Passage par variable

Considérons maintenant le programme suivant :

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (12 of 15) [23-09-2001 13:42:24]


Sémantique

program pval;
var x : integer;
procedure p (var a : integer);
begin
Ecrire ("a = ", a);
a := 0;
end;
begin
p (10); (* erreur ! *)
x := 10;
p (x);
Ecrire ("x = ", x);
end.

Il s'agit du même programme, mais a est déclaré comme passé par variable.
Lors de l'appel, le paramètre formel a est ajouté à l'environnement, comme dans le passage par valeur.
Mais, au contraire du passage par valeur où un nouvel emplacement libre est affecté au paramètre formel,
l'emplacement du paramètre formel est le même que celui du paramètre réel. On a donc deux variables
(ici x et a) qui partagent le même emplacement. Toute modification de a affecte alors x puisqu'il s'agit, en
fait, de la même zone mémoire.
Pour que ce mode de passage soit possible, il faut que le paramètre réel dénote une emplacement, et non
pas une valeur. L'appel p(10) est alors illégal, puisque 10 ne peut être évalué comme un emplacement. Il
en serait de même d'un appel de la forme p(x+y) ou p(f(x)). Par contre p(x), p(t[i]), p(c.x) sont des appels
licites car le paramètre réel est une variable dont on peut évaluer l'emplacement par Empl[[ ]].
Au retour de l'appel, le paramètre a est retiré de l'environnement, mais, contrairement à un paramètre
passé par valeur, son emplacement reste accessible par l'intermédiaire du paramètre réel x. Dans
l'exemple ci-dessus, après l'appel p(x), x vaut 0. C'est une seconde forme d'effet de bord, puisque la
mémoire de l'appelant a été modifiée d'une façon visible.

Usage du passage par variable

Les passages de paramètres par variable sont utilisés en Pascal pour permettre de retourner des types
complexes. En effet, le type de retour d'une fonction Pascal ne peut être qu'un type simple (entier, réel,
énuméré). La seule façon pour une fonction de retourner, par exemple, un tableau, est de prendre le
tableau comme paramètre par variable, et de le modifier :
program demo;
type tab = array [1..10] of integer;
var tablo : tab;
procedure zero (var t : tab);
var i : integer;
begin
for i := 1 to 10 do t[i] := 0;
fin;

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (13 of 15) [23-09-2001 13:42:24]


Sémantique

begin
zero (tablo); (* au lieu de : tablo := zero *)
end.

Le passage par variable est aussi indispensable lorsque l'on souhaite effectivement produire une effet de
bord :
program echange;
var x, y : integer;
procedure swap (var x : integer; var y : integer)
var tmp : integer;
begin
tmp := x;
x := y;
y := tmp;
end;
begin
Lire (x); Lire (y);
swap (x, y);
Ecrire ("x = ", x, "; y = ", y);
end.

Si x et y n'étaient pas passés par variable, la procédure ne produirait aucun effet perceptible !
Appels récursifs

Lorsqu'une procédure ou fonction s'appelle récursivement, le principe reste le même. Il en résulte que
chaque appel enrichit l'environnement, et donc chaque paramètre formel x de la procédure ou fonction
récursive masque celui de l'appel englobant :
program factorielle;
var a : integer;
function fact (x : integer) : integer;
begin
if x <= 1
then fact := 1
else fact := x * fact (x - 1);
end;
begin
Lire (a);
Ecrire (fact (a));
end.

Lorsque l'on calcule fact (3), on a successivement (les identificateurs entre parenthèses sont ceux qui sont
masqués) :

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (14 of 15) [23-09-2001 13:42:24]


Sémantique

env : x @1 mem : @1 3
env : (x) x @1 @2 mem : @1 @2 3 2
env : (x) @1 mem : @1 3 2 1
(x) x @2 @3 @2 @3

On voit que chaque appel de fact contient "son" x, les autres étant inaccessibles mais néanmoins présents.
On comprend également que la place mémoire nécessaire est proportionnelle au nombre d'appels
récursifs. Une récursion infinie va donc se solder par un débordement de la mémoire.
Les pointeurs

(à rédiger)
Contrôle de types

(à rédiger - pas fait en cours)

Autres chapitres :
1. Introduction - Langage de réalisation
2. Structures de données
3. Lexique et syntaxe
4. Sémantique (ce chapitre)

Michel Beaudouin-Lafon, mbl@lri.fr

http://www-ihm.lri.fr/~mbl/ENS/DEUG/cours/4-semantique.html (15 of 15) [23-09-2001 13:42:24]