Probleme – Analyser une expression arithmétique
Exercice · niveau 3 (difficile) · informatique (MP2I/MPI), chapitre 30 — Grammaires non contextuelles
Énoncé
On veut évaluer une expression écrite avec , , et des parenthèses.
- Partir de la grammaire à trois niveaux et dire pourquoi on ne peut pas la traduire directement.
- Écrire l'analyseur qui construit l'arbre de syntaxe abstraite.
- Vérifier priorités et associativité sur des cas choisis.
- Que doit faire l'analyseur sur une entrée malformée ?
Corrigé
1. Le point de départ, et l'obstacle.
Les trois niveaux encodent la priorité ; la récursivité à gauche encode l'associativité à gauche. Mais traduite littéralement, la règle donne une fonction qui s'appelle elle-même sans avoir rien consommé : pas de variant, pas de terminaison.
On la remplace par une boucle : . Et l'on accumule à gauche dans la boucle, ce qui rend l'associativité que la réécriture avait perdue.
2. L'analyseur.
exception Echec of string
type expr =
| Ent of int
| Plus of expr * expr
| Moins of expr * expr
| Fois of expr * expr
(* Analyse texte et rend son arbre de syntaxe abstraite.
Leve Echec avec la position fautive si le texte est malforme.
Complexite : Theta(|texte|) -- chaque caractere est lu une seule fois. *)
let analyser texte =
let n = String.length texte in
let pos = ref 0 in
let regarde () = if !pos < n then Some texte.[!pos] else None in
let lire c =
if !pos < n && texte.[!pos] = c then incr pos
else raise (Echec (Printf.sprintf "attendu %c en position %d" c !pos))
in
let entier () =
let debut = !pos in
while !pos < n && texte.[!pos] >= '0' && texte.[!pos] <= '9' do incr pos done;
if !pos = debut then raise (Echec (Printf.sprintf "chiffre attendu en %d" !pos));
Ent (int_of_string (String.sub texte debut (!pos - debut)))
in
(* E -> T { (+|-) T } : la BOUCLE remplace la recursion a gauche.
VARIANT : n - !pos decroit strictement a chaque tour, car lire consomme.
INVARIANT : acc contient l'arbre de tout ce qui a ete lu depuis le debut
de l'expression courante -- d'ou l'ASSOCIATIVITE A GAUCHE. *)
let rec e () =
let acc = ref (t ()) in
let continuer = ref true in
while !continuer do
match regarde () with
| Some '+' -> lire '+'; acc := Plus (!acc, t ())
| Some '-' -> lire '-'; acc := Moins (!acc, t ())
| _ -> continuer := false
done;
!acc
and t () =
let acc = ref (f ()) in
let continuer = ref true in
while !continuer do
match regarde () with
| Some '*' -> lire '*'; acc := Fois (!acc, f ())
| _ -> continuer := false
done;
!acc
and f () =
match regarde () with
| Some '(' -> lire '('; let x = e () in lire ')'; x
| _ -> entier ()
in
let x = e () in
if !pos <> n then raise (Echec (Printf.sprintf "reste a lire en %d" !pos));
x
let rec evalue = function
| Ent k -> k
| Plus (a, b) -> evalue a + evalue b
| Moins (a, b) -> evalue a - evalue b
| Fois (a, b) -> evalue a * evalue b
Trois fonctions, trois niveaux de la grammaire — et l'ordre des appels, appelant appelant , est la priorité des opérateurs. Rien d'autre ne l'encode.
3. Les vérifications. On affiche l'arbre entièrement parenthésé, ce qui rend sa forme visible, et l'on compare avec la variante fautive qui associerait à droite.
| Texte | Arbre construit | Valeur | Arbre à droite | Valeur |
|---|---|---|---|---|
| `8-3-2` | `((8-3)-2)` | `(8-(3-2))` | ||
| `2+3*4` | `(2+(3*4))` | `(2+(3*4))` | ||
| `(2+3)*4` | `((2+3)*4)` | `((2+3)*4)` | ||
| `1+2+3+4` | `(((1+2)+3)+4)` | `(1+(2+(3+4)))` | ||
| `10-2*3` | `(10-(2*3))` | `(10-(2*3))` | ||
| `1+2*3+4` | `((1+(2*3))+4)` | `(1+((2*3)+4))` |
Une seule ligne diffère, et c'est celle où l'opérateur n'est pas associatif : contre . Sur les cinq autres, le défaut d'associativité est invisible bien que l'arbre soit différent — voir la colonne 1+2+3+4, où les deux arbres sont franchement distincts et les deux valeurs égales.
Un test d'aller-retour clôt l'affaire : réanalyser le texte entièrement parenthésé rend le même arbre, sur les huit expressions essayées.
4. Les entrées malformées. L'analyseur doit refuser, et dire où. Chaque refus est mesuré :
| Entrée | Message |
|---|---|
| `1+` | chiffre attendu en |
| `*2` | chiffre attendu en |
| `(1+2` | attendu `)` en position |
| `1++2` | chiffre attendu en |
| `1 2` | reste à lire en |
| `1)` | reste à lire en |
| la chaîne vide | chiffre attendu en |
Les deux dernières familles sont distinctes, et c'est ce qui compte : « reste à lire » vient du contrôle final, les autres viennent d'un échec interne. Un analyseur qui oublierait le contrôle final accepterait 1) et 1 2 — c'est la faute de l'exercice sur le contrôle de fin.
Les autres exercices de ce chapitre Le cours du chapitre
Un blocage sur cet exercice ? Le tuteur d'Adloun guide par questions, sans donner la réponse.