Adloun

Analyse syntaxique et interprétation

Cours complet · OCaml (option informatique), chapitre 21 · prépas MPSI et MP, option informatique

Travailler ce chapitre sur Adloun Exercices corrigés de ce chapitre

<i class="fa-solid fa-compass mr-2" style="color:#9A563B"></i>21.1 Introduction et motivation

Comment une machine comprend-elle &quot;3 + 4 * 2&quot; et répond 11 (et non 14) ? C'est le travail d'un interpréteur, et c'est l'occasion d'un projet qui rassemble presque tout le livre. Le traitement se fait en trois étapes, une véritable chaîne de montage :

texte \;\; lexèmes \;\; arbre de syntaxe \;\; valeur

L'analyse lexicale (lexing) découpe la chaîne (chapitre 6) en lexèmes (nombres, opérateurs, parenthèses) ; l'analyse syntaxique (parsing) les organise en un arbre de syntaxe (un type somme récursif, chapitre 3) qui respecte les priorités ; l'évaluation parcourt cet arbre (comme au chapitre 3). Le parser s'écrit par descente récursive — un bel usage de la récursion mutuelle (chapitre 1).

21.2 Analyse lexicale

Un lexème est une unité de sens. Pour l'arithmétique, on en a six :


type lexeme = Nombre of int | Plus | Moins | Fois | ParenG | ParenD

L'analyseur lexical parcourt la chaîne, ignore les espaces, reconnaît les opérateurs et les parenthèses, et regroupe les chiffres consécutifs en un nombre.


let rec renverse l =          (* utilitaire (chapitre 2) *)
  let rec aux acc l = match l with [] -> acc | x :: r -> aux (x :: acc) r in
  aux [] l

let lexer s =
  let n = String.length s in
  let lex = ref [] and i = ref 0 in
  while !i < n do
    let c = s.[!i] in
    if c = ' ' then i := !i + 1
    else if c = '+' then begin lex := Plus :: !lex; i := !i + 1 end
    else if c = '-' then begin lex := Moins :: !lex; i := !i + 1 end
    else if c = '*' then begin lex := Fois :: !lex; i := !i + 1 end
    else if c = '(' then begin lex := ParenG :: !lex; i := !i + 1 end
    else if c = ')' then begin lex := ParenD :: !lex; i := !i + 1 end
    else if c >= '0' && c <= '9' then begin
      let v = ref 0 in
      while !i < n && s.[!i] >= '0' && s.[!i] <= '9' do
        v := !v * 10 + (int_of_char s.[!i] - int_of_char '0');
        i := !i + 1
      done;
      lex := Nombre !v :: !lex
    end
    else failwith "caractère inattendu"
  done;
  renverse !lex

lexer &quot;3 + 42&quot; renvoie [Nombre 3; Plus; Nombre 42]. On accumule en tête (donc à l'envers), puis on renverse. La boucle interne lit un nombre à plusieurs chiffres comme au chapitre 6.

21.3 L'arbre de syntaxe

On représente l'expression par un arbre (chapitre 3), où la structure encode les priorités : dans l'arbre de 3 + 4 2, le Fois est plus bas* que le Add, donc évalué d'abord.


type expr =
  | Const of int
  | Add of expr * expr
  | Sub of expr * expr
  | Mul of expr * expr
ImportantPriorités et associativité

Le est prioritaire sur le + : 3 + 4 2 doit donner l'arbre Add (Const 3, Mul (Const 4, Const 2)). De plus - est associatif à gauche : 8 - 3 - 2 vaut (8 - 3) - 2 = 3 et non 8 - (3 - 2) = 7. C'est l'analyseur syntaxique qui construit l'arbre correct ; l'évaluation n'a plus qu'à le suivre.

21.4 Analyse syntaxique par descente récursive

On décrit la syntaxe par une grammaire à trois niveaux, du moins prioritaire au plus prioritaire :

expression terme ( (`+` `-`) terme )
terme facteur ( `*` facteur )
facteur nombre `(` expression `)`

À chaque niveau correspond une fonction, qui consomme des lexèmes et renvoie un couple (arbre construit, lexèmes restants). Les fonctions s'appellent mutuellement (let rec … and …, chapitre 1).


let rec parse_expr lex =
  let (g, reste) = parse_terme lex in
  boucle_add g reste
and boucle_add g lex =
  match lex with
  | Plus :: reste  -> let (d, r) = parse_terme reste in boucle_add (Add (g, d)) r
  | Moins :: reste -> let (d, r) = parse_terme reste in boucle_add (Sub (g, d)) r
  | _ -> (g, lex)
and parse_terme lex =
  let (g, reste) = parse_facteur lex in
  boucle_mul g reste
and boucle_mul g lex =
  match lex with
  | Fois :: reste -> let (d, r) = parse_facteur reste in boucle_mul (Mul (g, d)) r
  | _ -> (g, lex)
and parse_facteur lex =
  match lex with
  | Nombre v :: reste -> (Const v, reste)
  | ParenG :: reste ->
      let (e, r) = parse_expr reste in
      (match r with
       | ParenD :: r2 -> (e, r2)
       | _ -> failwith "parenthese fermante attendue")
  | _ -> failwith "facteur attendu"

Les fonctions boucle_add et boucle_mul enchaînent les opérations à gauche : en construisant Add (g, d) puis en continuant avec ce résultat comme nouveau membre gauche, on obtient l'associativité à gauche. La descente expr → terme → facteur encode les priorités : un + ne se forme qu'après avoir entièrement analysé les *.

21.5 Interprétation

On assemble la chaîne complète : lexer, parser, évaluer (l'évaluation est celle du chapitre 3, étendue à Sub).


let rec evalue e =
  match e with
  | Const n -> n
  | Add (a, b) -> evalue a + evalue b
  | Sub (a, b) -> evalue a - evalue b
  | Mul (a, b) -> evalue a * evalue b

let interprete s =
  let (e, reste) = parse_expr (lexer s) in
  if reste <> [] then failwith "expression mal formee"
  else evalue e

interprete &quot;3 + 4 2&quot; vaut 11 ; interprete &quot;(3 + 4) 2&quot; vaut 14. Le test reste &lt;&gt; [] rejette les entrées mal formées (lexèmes en trop, comme &quot;3 4&quot;).

<i class="fa-solid fa-dumbbell mr-2" style="color:#2E7559"></i>21.6 Exercices résolus

Niveau (application directe du cours)

Exercice 1 : Le type des lexèmes

Définir lexeme et donner, à la main, le résultat attendu de lexer &quot;(1+2)&quot;.

Démonstration

type lexeme = Nombre of int | Plus | Moins | Fois | ParenG | ParenD
(* lexer "(1+2)" = [ParenG; Nombre 1; Plus; Nombre 2; ParenD] *)

Chaque caractère significatif devient un lexème ; les nombres regroupent leurs chiffres. C'est l'alphabet de notre mini-langage.

Exercice 2 : Évaluer un arbre

Écrire evalue et évaluer Add (Const 3, Mul (Const 4, Const 2)).

Démonstration

let rec evalue e =
  match e with
  | Const n -> n
  | Add (a, b) -> evalue a + evalue b
  | Sub (a, b) -> evalue a - evalue b
  | Mul (a, b) -> evalue a * evalue b

L'arbre Add (Const 3, Mul (Const 4, Const 2)) s'évalue en 3 + (4 * 2) = 11 : la structure de l'arbre impose déjà l'ordre des opérations.

Exercice 3 : Lexer à un chiffre

Écrire une version simplifiée de lexer pour des chiffres uniques (pas de nombres à plusieurs chiffres), sur une chaîne sans espaces.

Démonstration

let lexer_simple s =
  let lex = ref [] in
  for i = String.length s - 1 downto 0 do      (* à l'envers : pas de renverse *)
    let c = s.[i] in
    let l =
      if c = '+' then Plus else if c = '-' then Moins
      else if c = '*' then Fois else if c = '(' then ParenG
      else if c = ')' then ParenD
      else Nombre (int_of_char c - int_of_char '0')
    in
    lex := l :: !lex
  done;
  !lex

En parcourant de droite à gauche et en ajoutant en tête, on obtient directement la liste dans le bon ordre, sans renversement. (downto déborde du sous-ensemble strict ; avec to et un renversement final, on retrouve la version du cours.)

Niveau (raisonnement intermédiaire)

Exercice 4 : Lexer complet

Écrire lexer gérant les nombres à plusieurs chiffres et les espaces.

Démonstration

let lexer s =
  let n = String.length s in
  let lex = ref [] and i = ref 0 in
  while !i < n do
    let c = s.[!i] in
    if c = ' ' then i := !i + 1
    else if c = '+' then begin lex := Plus :: !lex; i := !i + 1 end
    else if c = '-' then begin lex := Moins :: !lex; i := !i + 1 end
    else if c = '*' then begin lex := Fois :: !lex; i := !i + 1 end
    else if c = '(' then begin lex := ParenG :: !lex; i := !i + 1 end
    else if c = ')' then begin lex := ParenD :: !lex; i := !i + 1 end
    else if c >= '0' && c <= '9' then begin
      let v = ref 0 in
      while !i < n && s.[!i] >= '0' && s.[!i] <= '9' do
        v := !v * 10 + (int_of_char s.[!i] - int_of_char '0');
        i := !i + 1
      done;
      lex := Nombre !v :: !lex
    end
    else failwith "caractere inattendu"
  done;
  renverse !lex

La boucle interne accumule un nombre tant qu'on lit des chiffres (v := !v * 10 + chiffre). On accumule les lexèmes à l'envers, d'où le renverse final.

Exercice 5 : Analyser un facteur

Écrire parse_facteur (un nombre ou une expression parenthésée). Pourquoi a-t-il besoin de parse_expr ?

Démonstration

let rec parse_facteur lex =
  match lex with
  | Nombre v :: reste -> (Const v, reste)
  | ParenG :: reste ->
      let (e, r) = parse_expr reste in
      (match r with
       | ParenD :: r2 -> (e, r2)
       | _ -> failwith "parenthese fermante attendue")
  | _ -> failwith "facteur attendu"
and parse_expr lex = ...   (* mutuellement récursif *)

Un facteur entre parenthèses contient une expression complète : parse_facteur doit donc appeler parse_expr, qui à son tour redescend jusqu'aux facteurs. D'où la récursion mutuelle (let rec … and …, chapitre 1) entre tous les niveaux de la grammaire.

Exercice 6 : Priorité du produit

Expliquer, sur lexer &quot;3 + 4 * 2&quot;, comment la grammaire à trois niveaux donne l'arbre correct.

Démonstration

parse_expr appelle d'abord parse_terme, qui analyse 3 (un facteur) ; comme le lexème suivant est Plus (pas Fois), boucle_mul s'arrête : le premier terme est Const 3. De retour dans boucle_add, on voit Plus, donc on analyse un second terme : parse_terme lit 4, voit Fois, et forme Mul (Const 4, Const 2). Résultat : Add (Const 3, Mul (Const 4, Const 2)). Le a été regroupé avant* le +, car il vit à un niveau plus profond de la grammaire.

Niveau (approfondissement)

Exercice 7 : L'analyseur complet

Écrire parse_expr et ses fonctions associées (descente récursive), gérant +, -, * et les parenthèses.

Démonstration

let rec parse_expr lex =
  let (g, reste) = parse_terme lex in
  boucle_add g reste
and boucle_add g lex =
  match lex with
  | Plus :: reste  -> let (d, r) = parse_terme reste in boucle_add (Add (g, d)) r
  | Moins :: reste -> let (d, r) = parse_terme reste in boucle_add (Sub (g, d)) r
  | _ -> (g, lex)
and parse_terme lex =
  let (g, reste) = parse_facteur lex in
  boucle_mul g reste
and boucle_mul g lex =
  match lex with
  | Fois :: reste -> let (d, r) = parse_facteur reste in boucle_mul (Mul (g, d)) r
  | _ -> (g, lex)
and parse_facteur lex =
  match lex with
  | Nombre v :: reste -> (Const v, reste)
  | ParenG :: reste ->
      let (e, r) = parse_expr reste in
      (match r with ParenD :: r2 -> (e, r2) | _ -> failwith "parenthese attendue")
  | _ -> failwith "facteur attendu"

Cinq fonctions mutuellement récursives reflètent la grammaire. Les boucle_* réalisent les répétitions « » en associant à gauche.

Exercice 8 : L'interpréteur

Assembler interprete et donner les valeurs de &quot;2 * (3 + 4) - 5&quot; et &quot;10 - 3 - 2&quot;.

Démonstration

let interprete s =
  let (e, reste) = parse_expr (lexer s) in
  if reste <> [] then failwith "expression mal formee" else evalue e

interprete &quot;2 * (3 + 4) - 5&quot; . interprete &quot;10 - 3 - 2&quot; (associativité à gauche, grâce à boucle_add). La chaîne lexer parser evalue produit la bonne valeur en respectant priorités et associativité.

Exercice 9 : Ajouter la division

Étendre l'interpréteur avec la division / (même priorité que *). Indiquer les trois endroits à modifier.

Démonstration

Trois ajouts cohérents traversent toute la chaîne :

  • le lexème : ajouter Div au type lexeme, et le cas '/' dans lexer ;
  • l'arbre : ajouter Divi of expr * expr au type expr ;
  • le parser : dans boucle_mul, traiter Div :: reste comme Fois (même niveau) en construisant Divi ;
  • l'évaluateur : le cas Divi (a, b) -&gt; evalue a / evalue b.

| Div :: reste -> let (d, r) = parse_facteur reste in boucle_mul (Divi (g, d)) r
(* ... *)
| Divi (a, b) -> evalue a / evalue b

À chaque ajout d'un constructeur à l'arbre, le compilateur signale les match devenus non exhaustifs (chapitre 3) — ici celui d'evalue : il guide l'extension. Mais un filtrage qui finit par _ reste muet : c'est le cas de ceux de l'analyseur sur les lexèmes, qu'il faut donc revoir soi-même. Une grammaire bien structurée se prolonge sans effort.

Exercice 10 : Détecter les erreurs de syntaxe

Donner trois entrées mal formées et expliquer comment l'interpréteur les rejette.

Démonstration
  • &quot;3 +&quot; : boucle_add voit Plus, appelle parse_terme sur la liste vide ; parse_facteur [] ne filtre ni Nombre ni ParenG failwith &quot;facteur attendu&quot;.
  • &quot;(3 + 4&quot; : la parenthèse fermante manque ; parse_facteur ne trouve pas ParenD failwith &quot;parenthese attendue&quot;.
  • &quot;3 4&quot; : parse_expr analyse 3, mais il reste [Nombre 4] non consommé ; le test reste &lt;&gt; [] dans interprete le rejette.

Un analyseur correct refuse les entrées invalides au lieu de produire un résultat absurde — c'est la rigueur du contrat (chapitre 1).

Synthèse du chapitre (à retenir)
  • Un interpréteur est une chaîne : texte (analyse lexicale) lexèmes (analyse syntaxique) arbre de syntaxe (évaluation) valeur.
  • Lexer : parcourir la chaîne (chapitre 6), produire une liste de lexèmes ; regrouper les chiffres en nombres.
  • Arbre de syntaxe : type somme récursif (chapitre 3) dont la structure encode priorités et associativité.
  • Descente récursive : une fonction par niveau de grammaire (expression terme facteur), mutuellement récursives (chapitre 1) ; chaque fonction renvoie (arbre, lexèmes restants) ; les boucles boucle_* donnent l'associativité à gauche.
  • Évaluer : parcours récursif de l'arbre (chapitre 3). Rejeter les entrées mal formées (lexèmes restants, lexème inattendu).
  • Étendre le langage (nouvel opérateur) modifications cohérentes du lexème, de l'arbre, du parser et de l'évaluateur — le typeur guide, sauf là où un _ final avale le nouveau cas.

21.7 Exercices d'entraînement

Légende : application directe, raisonnement intermédiaire, approfondissement ; signale un classique incontournable. La numérotation prolonge celle des dix exercices résolus.

Thème A — Lexique.
Thème B — Arbre et évaluation.
Thème C — Extensions du parser.
Thème D — Vers un vrai langage.

Continuer sur Adloun : animation, QCM, fiches, exercices