Syntaxe
Grâce à l'analyse lexicale, nous sommes à même de découper
notre flot d'entrée en unités plus structurées : les unités
lexicales. Il faut encore savoir assembler ces unités de façon
à former des phrases syntaxiquement correctes d'un langage donné.
Les règles d'assemblage syntaxique sont définies au moyen de
règles de grammaires. C'est un formalisme originaire de la
linguistique que les mathématiciens théoriciens des langages et
les informaticiens ont su reprendre avec profit. Nous avons déjà
vu par anticipation page ??, un exemple de
grammaire pour le langage Basic. Nous allons reprendre cet exemple afin
d'introduire les concepts de base des grammaires.
Grammaire
Formellement, une grammaire est constituée de quatre
éléments :
-
Un ensemble de symboles dits terminaux. Ces symboles
représentent les unités lexicales du langage. En Basic, on y
trouve les symboles opérateurs et de relations arithmétiques et
logiques (+, &, <, <=, ..) ; les mots clés du
langage (GOTO, PRINT, IF, THEN, ..) ; les
entiers (unité entier) et les variables (unité variable).
- Un ensemble de symboles dits non terminaux. Ces symboles
représentent les composantes syntaxique du langage. Par exemple, un
programme Basic est composé de lignes, on a donc la composante Ligne ; une ligne peut contenir une Expression, etc.
- Un ensemble de règles dites de production. Elles décrivent
comment s'articulent les symboles terminaux et ceux non terminaux pour
produire une composante syntaxique. Une ligne en Basic, est formée
de son numéro suivi d'une instruction. C'est le sens de la règle
suivante :
Ligne |
::= |
entier Instruction |
Une même composante peut être produite de plusieurs façons. On
sépare les alternatives par le symbole | comme dans
Instruction |
::= |
LET variable = Expression |
|
| |
GOTO entier |
|
| |
PRINT Expression |
etc. |
- Enfin, parmi tous les non terminaux, on en distingue un que l'on
appelle axiome. C'est sa règle de production qui dénote la
règle de production du langage dans son entier.
Production et reconnaissance
Les règles de production peuvent servir à reconnaître qu'une
suite de lexèmes appartient à un langage.
Considérons, par exemple un langage simple d'expressions
arithmétiques :
Exp |
::= |
entier |
(R1) |
|
| |
Exp + Exp |
(R2) |
|
| |
Exp * Exp |
(R3) |
|
| |
( Exp ) |
(R4) |
où (R1) (R2) (R3) et (R4) sont les noms que nous donnons à nos
règles. Après passage par l'analyse lexicale, l'expression
1*(2+3)
devient la suite de lexèmes :
entier * ( entier + entier )
Pour analyser cette phrase et reconnaître qu'elle appartient bien au
langage des expressions arithmétiques, nous allons utiliser nos
règles de droite à gauche : si une sous-expression correspond à
un membre droit de l'une des règles, nous la remplaçons par le
membre gauche correspondant et nous recommençons jusqu'à avoir
réduit notre expression au non terminal axiome (ici, Exp). Voici les étapes d'une telle analyse3.
entier * ( entier + entier ) |
¬¾(R1) |
Exp * ( entier + entier ) |
|
¬¾(R1) |
Exp * ( Exp + entier ) |
|
¬¾(R1) |
Exp * ( Exp + Exp ) |
|
¬¾(R2) |
Exp * ( Exp ) |
|
¬¾(R4) |
Exp * Exp |
|
¬¾(R3) |
Exp |
En partant de la dernière ligne qui ne contient plus que Exp
et en remontant les flèches, on lit comment a pu être produite
notre expression en partant de la règle axiome Exp : c'est
donc une phrase bien formée du langage défini par la grammaire.
La traduction des grammaires en programmes capables de reconnaître
qu'une suite de lexèmes appartient au langage défini par une
grammaire est un problème beaucoup plus complexe que celui de
l'utilisation des expressions rationnelles.
En effet, un résultat mathématique nous dit que tout ensemble (de
mots) défini au moyen du formalisme des expressions rationnelles
peut aussi être défini dans un autre formalisme : les
automates finis déterministes. Et ces derniers sont faciles à
exprimer comme des programmes prenant en entrée une suite de
caractères. On ne dispose pas d'un tel résultat pour le cas des
grammaires en général. Cependant, on dispose de résultats plus
faibles établissant l'équivalence entre certaines classes de
grammaires et des automates un peu plus riches : les automates
à pile. Nous ne voulons pas entrer dans les détails de ces
résultats ni donner une définition exacte de ce que sont les
automates. Cependant, nous ne pourrons pas faire l'économie d'une
petite étude des grammaires afin d'identifier quelle classe de
grammaire peut être utilisée dans les outils de génération
d'analyseurs syntaxiques ou pour implanter directement un analyseur.
Analyse descendante
La décomposition de l'expression 1*(2+3)
proposée au
paragraphe précédent n'est pas unique : on aurait pu tout aussi
bien commencer par réduire les entiers de droite à gauche, ce
qui aurait permis d'utiliser plutôt la règle (R2) pour réduire
d'abord 2+3
. Ces deux façons de faire constituent deux types
d'analyses : l'analyse ascendante (de droite à gauche) et l'analyse
descendante (de gauche à droite). Cette
dernière est facilement réalisable à partir de flots de
lexèmes en utilisant le module Stream. L'analyse ascendante
est celle réalisée par l'outil ocamlyacc, elle met en jeu
un mécanisme explicite de pile comme cela a déjà été
illustré pour l'analyse syntaxique des programmes Basic. Le choix
du type d'analyse n'est pas indifférent car suivant la forme de
grammaire utilisée pour spécifier un langage, on peut ou non
utiliser l'analyse descendante.
Le cas simple
Le cas d'école de l'analyse descendante est la notation préfixée
des expressions arithmétiques définie par :
Expr |
::= |
entier |
|
| |
+ Expr Expr |
|
| |
* Expr Expr |
Dans un tel cas, il suffit de connaître le premier lexème pour
savoir quelle règle de production a pu être utilisée. Cette
prédictibilité immédiate permet de ne pas gérer explicitement
la pile de l'analyse et de s'en remettre à l'empilement des appels
récursifs de l'analyseur. Il est alors très facile d'écrire un
programme implantant l'analyse descendante en utilisant les
fonctionnalités des modules Genlex et Stream.
La fonction infix_of ainsi obtenue prend une expression
préfixée et retourne son équivalent infixé.
# let
lexer
s
=
let
ll
=
Genlex.make_lexer
[
"+"
;"*"
]
in
ll
(Stream.of_string
s)
;;
val lexer : string -> Genlex.token Stream.t = <fun>
# let
rec
stream_parse
s
=
match
s
with
parser
[<
'Genlex.
Ident
x>]
->
x
|
[<
'Genlex.
Int
n>]
->
string_of_int
n
|
[<
'Genlex.
Kwd
"+"
;
e1=
stream_parse;
e2=
stream_parse>]
->
"("
^
e1^
"+"
^
e2^
")"
|
[<
'Genlex.
Kwd
"*"
;
e1=
stream_parse;
e2=
stream_parse>]
->
"("
^
e1^
"*"
^
e2^
")"
|
[<>]
->
failwith
"Parse error"
;;
val stream_parse : Genlex.token Stream.t -> string = <fun>
# let
infix_of
s
=
stream_parse
(lexer
s)
;;
val infix_of : string -> string = <fun>
# infix_of
"* +3 11 22"
;;
- : string = "((3+11)*22)"
Il faut prendre garde à ce que l'analyse lexicale est assez
fruste. Il est conseillé d'intercaler systématiquement un espace
entre les diverses unités lexicales de la chaîne d'entrée.
# infix_of
"*+3 11 22"
;;
- : string = "*+"
Un cas moins simple
L'analyse syntaxique utilisant les flots est prédictive. Elle impose
deux conditions sur les grammaires.
-
Il ne doit pas y avoir de récursivité gauche dans les
règles de grammaire. Une règle est récursive à gauche lorsque que le
membre droit commence par le non-terminal qui est le membre gauche de
la règle, comme dans Exp ::= Exp + Exp ;
- Il ne doit pas y avoir deux règles commençant par la même
expression.
La grammaire usuelle des expressions arithmétiques donnée page
?? ne convient pas directement à l'analyse
descendante : elle ne satisfait aucun des deux critères
ci-dessus. Pour pouvoir utiliser une analyse descendante, il faut
reformuler la grammaire de façon à supprimer la récursion
gauche et le non déterminisme des règles. Pour les expressions
arithmétiques, on peut donner, par exemple :
Expr |
::= |
Atom SuiteExpr |
SuiteExpr |
::= |
+ Atom |
|
| |
- Atom |
|
| |
* Atom |
|
| |
/ Atom |
|
| |
e |
Atom |
::= |
entier |
|
| |
( Expr ) |
Remarquons l'usage du mot vide e dans la définition de
SuiteExp. Il est nécessaire si nous voulons qu'un entier tout
seul soit une expression.
Notre grammaire permet l'implantation de l'analyseur
suivant qui n'est qu'une traduction simple des règles de
production. Cet analyseur produit l'arbre de syntaxe abstraite des
expressions arithmétiques.
# let
rec
suite
=
parser
[<
'Lsymbol
"+"
;
e2
=
atom
>]
->
Some
(PLUS,
e2)
|
[<
'Lsymbol
"-"
;
e2
=
atom
>]
->
Some
(MOINS,
e2)
|
[<
'Lsymbol
"*"
;
e2
=
atom
>]
->
Some
(MULT,
e2)
|
[<
'Lsymbol
"/"
;
e2
=
atom
>]
->
Some
(DIV,
e2)
|
[<
>]
->
None
and
atom
=
parser
[<
'Lint
i
>]
->
ExpInt
i
|
[<
'Lsymbol
"("
;
e
=
expr
;
'Lsymbol
")"
>]
->
e
and
expr
s
=
match
s
with
parser
[<
e1
=
atom
>]
->
match
suite
s
with
None
->
e1
|
Some
(op,
e2)
->
ExpBin(e1,
op,
e2)
;;
val suite : lexeme Stream.t -> (op_bin * expression) option = <fun>
val atom : lexeme Stream.t -> expression = <fun>
val expr : lexeme Stream.t -> expression = <fun>
La difficulté d'utilisation de l'analyse descendante vient de
la nécessité d'avoir des grammaires d'une forme très
restreinte. Et lorsque le langage visé se formule naturellement,
comme dans le cas des expressions infixées, avec une grammaire
récursive à gauche, il n'est pas toujours trivial de trouver une
grammaire équivalente (i.e. qui définit le même langage) qui
satisfasse aux exigences de l'analyse descendante. C'est pourquoi les
outils yacc et ocamlyacc mettent plutôt en oeuvre un
mécanisme d'analyse ascendante qui autorise la définition de
grammaires plus naturelles. Nous allons cependant voir que là encore,
tout ne sera pas possible.
Analyse ascendante
Page ??, nous avons présenté intuitivement les
principes de l'analyse ascendante : avancer (shift) ou
réduire (reduce). À chacune de ces actions l'état de
la pile est modifié. On peut déduire dette suite d'actions des règles
de grammaire, si toutefois, comme dans le cas de l'analyse
descendante, la grammaire s'y prète ! Ici encore, la difficulté
viendra du non déterminisme des règles qui ne permettra pas de choisir
entre avancer et réduire. Nous allons illustrer le fonctionnement de
l'analyse ascendente et ses causes d'échec en considérant les
sempiternelles expressions arithmétiques en notation postfixée et en
notation infixée.
Pour le meilleur
La grammaire simplifiée des expressions arithmétiques
postfixées est :
Expr |
::= |
chiffre |
(R1) |
|
| |
Expr Expr + |
(R2) |
|
| |
Expr Expr * |
(R3) |
Cette grammaire est duale de celle des expressions préfixées : il
faut attendre la fin de l'analyse pour savoir, en fin de compte,
quelle règle a été utilisée, mais une fois cette fin atteinte,
on sait exactement comment faire. En fait, l'analyse ascendente de
telles expressions est extrêmement proche d'un mécanisme
d'évaluation avec une pile. Simplement, au lieu d'empiler les
valeurs résultant du calcul, on empile les symboles de la
grammaire. L'idée étant que si l'on démarre avec une pile vide
il faut obtenir une pile ne contenant que le non terminal axiome
lorsque l'entrée est épuisée. Les modifications de la pile sont
les suivantes : si l'on avance, on empile le non terminal courant ;
si l'on réduit, c'est que les premiers éléments de la pile
correspondent à un membre droit (inversé) de règle, auquel cas
on remplace ces éléments par le membre gauche correspondant.
La figure 11.2 illustre
comment l'analyse ascendante traite l'expression : 1 2 + 3 * 4 +.
L'unité lexicale lue est soulignée. La fin de l'entrée est marquée par un $.
Action |
Entrée |
Pile |
|
12+3*4+$ |
[] |
Avancer |
|
|
2+3*4+$ |
[1] |
Réduire (R1) |
|
|
2+3*4+$ |
[Expr] |
Avancer |
|
|
|
+3*4+$ |
[2Expr] |
Réduire (R1) |
|
|
+3*4+$ |
[Expr Expr] |
Avancer, Réduire (R2) |
|
|
3*4+$ |
[Expr] |
Avancer, Réduire (R1) |
|
|
*4+$ |
[Expr Expr] |
Avancer, Réduire (R3) |
|
|
4+$ |
[Expr] |
Avancer, Réduire (R1) |
|
|
+$ |
[Expr Expr] |
Avancer, Réduire (R2) |
|
|
$ |
[Expr] |
Figure 11.2 : Exemple d'analyse ascendante
Pour le pire
Toute la difficulté du passage de la grammaire au programme de
reconnaissance du langage est de déterminer quel type d'action
appliquer. Nous allons illustrer cette difficulté sur trois exemples
provoquant trois genres d'indétermination.
Le premier exemple est une grammaire pour les expressions n'utilisant
que l'addition :
E0 |
::= |
entier |
(R1) |
|
| |
E0 + E0 |
(R2) |
L'indécision de cette grammaire vient de la règle (R2). Supposons
la situation suivante:
Action |
Entrée |
Pile |
: |
|
|
|
+... |
[E0 + E0 ...] |
: |
|
|
Dans un tel cas, il n'est pas possible de déterminer s'il faut avancer
et empiler le + ou réduire selon (R2) les deux E0 et le
+ présents dans la pile. Nous sommes en présence d'un conflit
avancer-réduire (shift/reduce). Il provient du fait que
l'expression entier + entier + entier peut être
produite, par dérivation droite, de deux façons.
Première façon : |
E0 |
¾®(R2) |
E0 + E0 |
|
|
¾®(R1) |
E0 + entier |
|
|
¾®(R2) |
E0 + E0 + entier |
|
etc. |
Seconde façon : |
E0 |
¾®(R2) |
E0 + E0 |
|
|
¾®(R2) |
E0 + E0 + E0 |
|
|
¾®(R1) |
E0 + E0 + entier |
|
etc. |
Les expressions obtenues par les deux dérivations peuvent sembler
similaire du point de vue de l'évaluation d'une expression :
(entier + entier) + entier et
entier + (entier + entier)
mais diffèrent pour la construction d'un arbre syntaxique (voir
figure 6.3 page ??).
Le deuxième exemple de grammaire engendrant un conflit entre avancer et
réduire relève du même genre d'ambigüité : un parenthésage
implicite. Mais contrairement au cas précédent, le choix en
avancer et réduire modifie le sens de l'expression analysée. Soit
donc la grammaire :
E1 |
::= |
entier |
(R1) |
|
| |
E1 + E1 |
(R2) |
|
| |
E1 * E1 |
(R3) |
On retrouve avec cette grammaire le même conflit que ci-dessus à
la fois pour + et *. Mais s'y rajoute un conflit entre
+ et *. Ici encore, une même expression peut être
produite de deux façons. Il existe deux dérivations droite de
entier + entier * entier
Première façon : |
E1 |
¾®(R3) |
E1 * E1 |
|
|
¾®(R1) |
E1 * entier |
|
|
¾®(R2) |
E1 + E1 * entier |
|
etc. |
Seconde façon : |
E1 |
¾®(R2) |
E1 + E1 |
|
|
¾®(R3) |
E1 + E1 * E1 |
|
|
¾®(R1) |
E1 + E1 * entier |
|
etc. |
Ici, les deux paranthésages (implicites) ne sont pas équivalents :
(entier + entier) * entier
¹ entier + (entier * entier)
Ce problème a déjà été rencontré pour les expressions
Basic (voir page ??). Il a été résolu en
attribuant des priorités aux opérateurs : on réduit (R3) avant
(R2). Ce qui revient à parenthéser les produits.
On peut également résoudre le problème du choix entre + et * en modifiant la grammaire. On introduit deux nouveaux terminaux :
T, pour terme, et F, pour facteur. Ce qui donne :
E |
::= |
E + T |
(R1) |
|
| |
T |
(R2) |
T |
::= |
T * F |
(R3) |
|
| |
F |
(R4) |
F |
::= |
entier |
(R5) |
Il n'y a maintenant plus qu'une seule façon de démarrer la
séquence de production de entier + entier *
entier : en utilisant la règle (R1).
Le troisième et dernier exemple concerne les instructions
conditionnelles des langages de programmation. Un langage comme
Pascal
offre deux conditionnelles : le if .. then et le if .. then
.. else. Imaginons la grammaire suivante :
Instr |
::= |
if Exp then Instr |
(R1) |
|
| |
if Exp then Instr else Instr |
(R2) |
|
| |
etc... |
Et dans la situation suivante :
Action |
Entrée |
Pile |
: |
|
|
|
else... |
[Instr then Exp if...] |
: |
|
|
On ne peut pas déterminer si les premiers éléments de la pile
correspondent à la conditionnelle de (R1), au quel cas il faut
réduire, ou au premier Instr de la règle (R2), auquel cas il
faut avancer.
Outre les conflits avancer-réduire, l'analyse ascendante provoque
également des conflits réduire-réduire.
Nous présentons maintenant l'outil ocamlyacc qui utilise cette
technique et peut rencontrer ces conflits.
L'outil ocamlyacc
L'outil ocamlyacc est construit sur le même principe que ocamllex : il prend en entrée un fichier de description d'une
grammaire dont chaque règle est attachée à une action sémantique et il
engendre deux fichiers Objective CAML d'extension .ml et .mli
contenant la fonction d'analyse syntaxique et son interface.
Format général
Les fichiers de description syntaxique pour ocamlyacc utilisent
par convention l'extension .mly et ont le format suivant :
%{ |
|
|
en-tête |
}% |
|
|
déclarations |
%% |
|
|
règles |
%% |
|
|
suite-et-fin |
Le format d'une règle est :
non-terminal |
: |
symbole...symbole |
{ action sémantique } |
|
| |
... |
|
| |
symbole...symbole |
{ action sémantique } |
|
; |
Un symbole est soit un terminal, soit un non terminal. Les
sections << en-tête >> et << suite-et-fin >> jouent le même rôle
qu'en ocamllex à la différence près que l'en-tête
n'est visible que des règles et non des déclarations. En particulier,
cela entraîne que les ouvertures de module (open)
ne sont pas prises en compte dans la partie déclarative et donc que
les types doivent être qualifiés.
Actions sémantiques
Les actions sémantiques sont des morceaux de code Objective CAML
exécutés lorsque l'analyseur réduit la règle à laquelle
elles sont associées. Le corps d'une action sémantique peut faire
référence aux composants du membre droit de la règle. Ceux-ci
sont numérotés de gauche à droite en commençant par 1. Le
premier composant est référencé par $1, le deuxième par
$2, etc.
Axiomes
On peut déclarer plusieurs non terminaux axiomes de la grammaire en
écrivant, dans la partie déclaration :
%start non-terminal .. non-terminal
Pour chacun d'eux sera engendré une fonction d'analyse. Il faut
préciser, toujours dans la partie déclaration, quel sera le type
de retour de ces fonctions.
%type <type-retour> non-terminal
Le type-retour
doit être qualifié.
Warning
Les symboles non terminaux deviennent les noms des fonctions
d'analyse. Ils ne doivent donc pas commencer par une majuscule qui est
l'initiale réservée aux constructeurs.
Unités lexicales
Les règles de grammaire font référence aux unités lexicales
qui sont les symboles terminaux des règles.
Un (ou des) lexème(s) se déclare(nt) de la façon suivante :
%token PLUS MOINS MULT DIV MOD
Certaines unités lexicales, comme les identificateurs,
représentent un ensemble de chaînes de caractères. Lorsque l'on
rencontre un identificateur on veut pouvoir récupérer cette
chaîne. On indique à l'analyseur lexical que ces lexèmes ont une
valeur associée en donnant entre <
et >
le type de cette
valeur :
%token <string> IDENT
Warning
Après traitement par ocamlyacc toutes ces déclarations sont
transformées en constructeurs du type token. Ils doivent
donc impérativement commencer par une majuscule.
On peut utiliser des chaînes de caractères comme terminaux
implicites comme dans :
expr : expr "+" expr { ... }
| expr "*" expr { ... }
| ...
;
auquel cas, il est inutile de déclarer un symbole les
représentant : ils sont directement traités par l'analyseur
syntaxique sans avoir à passer par l'analyseur lexical. Par soucis
d'uniformité, nous ne conseillons cependant pas cette manière de
faire.
Priorité, associativité
Nous avons vu que nombre de conflits de l'analyse ascendante
proviennent de règles implicites d'association d'un opérateur ou
de conflits de priorité entre opérateurs. Pour éliminer ces
conflits, on peut déclarer des règles d'associativité par
défaut (à gauche ou à droite ou aucune) pour les opérateurs
ainsi que des règles de priorité. La déclaration suivante
indique que les opérateurs + (lexème PLUS) et * (lexème MULT) sont associatifs à
droite par défaut et que * a une priorité supérieure
à celle de + car MULT est déclaré après PLUS.
%left PLUS
%left MULT
Deux opérateurs déclarés sur une même ligne ont même
priorité.
Options de la commande
ocamlyacc possède deux options :
-
-b nom : les fichiers Objective CAML engendrés sont nom.ml
et nom.mli;
- -v : crée un fichier d'extension .output contenant la numérotation
des règles, les états de l'automate reconnaissant la grammaire et
les sources de conflits.
Utilisation conjointe avec ocamllex
On peut composer les deux outils ocamllex et ocamlyacc de sorte
que la transformation du flot de caractères en flot de lexèmes
soit l'entrée de l'analyseur syntaxique. Pour se faire, Le type lexeme doit être commun aux deux. Ce type est défini dans les fichiers d'extension .mli
et .ml engendrés par ocamlyacc à partir des déclarations des
token du fichier d'extension .mly correspondant. Le
fichier .mll importe ce type; ocamllex traduit ce fichier
en une fonction Objective CAML de type Lexing.lexbuf -> lexeme.
L'exemple page ?? illustre cette interaction
et décrit les différentes phases de compilation.
Grammaires contextuelles
Les analyseurs engendrés par ocamlyacc traitent de langages
engendrés par des grammaires dites non contextuelles. La
poursuite de l'analyse d'un flot de lexème ne dépend pas des
valeurs syntaxiques déjà traitées. Ce n'est pas le cas du
langage L décrit par la formule suivante :
L ::= wCw | w avec w Î (A|B)*
où A, B et C sont des symboles terminaux. On a écrit wCw
(avec w Î (A|B)*) et non simplement (A|B)*C(A|B)* car nous voulons
avoir le même mot à gauche et à droite du C médian.
Pour analyser les mots de L, il faut garder en mémoire ce qui a
été rencontré avant la lettre C pour vérifier que l'on retrouve
bien la même chose après. Voici une solution à ce problème qui
utilise le parcours d'un flot. L'idée générale de l'algorithme
est de construire une fonction d'analyse de flot qui reconnaîtra
exactement le sous-mot figurant avant l'éventuelle occurrence de C :
On se donne le type :
# type
token
=
A
|
B
|
C
;;
La fonction parse_w1 construit la fonction de mémorisation
du premier w sous forme d'une liste de fonctions d'analyse de flot
atomique (i.e. pour un seul token) :
# let
rec
parse_w1
s
=
match
s
with
parser
[<
'A;
l
=
parse_w1
>]
->
(parser
[<
'A
>]
->
"a"
)::l
|
[<
'B;
l
=
parse_w1
>]
->
(parser
[<
'B
>]
->
"b"
)::l
|
[<
>]
->
[]
;;
val parse_w1 : token Stream.t -> (token Stream.t -> string) list = <fun>
Le résultat des fonctions construites par parse_w1 est
simplement la chaîne de caractères contenant l'unité lexicale
analysée.
La fonction parse_w2 prend en argument une liste construite
par parse_w1 pour composer chacun de ses éléments en une
seule fonction d'analyse :
# let
rec
parse_w2
l
=
match
l
with
p::pl
->
(parser
[<
x
=
p;
l
=
(parse_w2
pl)
>]
->
x^
l)
|
[]
->
parser
[<>]
->
""
;;
val parse_w2 : ('a Stream.t -> string) list -> 'a Stream.t -> string = <fun>
Le résultat de l'application de parse_w2 sera la chaîne
représentant le sous-mot w. Par construction, la fonction
parse_w2 ne pourra reconnaître que le sous-mot parcouru
par parse_w1.
En utilisant la possibilité de nommer dans les flots les résultats
intermédiaires, on écrit la fonction de reconnaissance des mots du
langage L :
# let
parse_L
=
parser
[<
l
=
parse_w1
;
'C;
r
=
(parse_w2
l)
>]
->
r
;;
val parse_L : token Stream.t -> string = <fun>
Voici deux petits exemples. Le premier donne en résultat la chaîne
contenant le mot entourant C, le second échoue car les deux mots
entourant C sont différents :
# parse_L
[<
'A;
'B;
'B;
'C;
'A;
'B;
'B
>]
;;
- : string = "abb"
# parse_L
[<
'A;
'B;
'C;
'B;
'A
>]
;;
Uncaught exception: Stream.Error("")