Consignes
Le rendu de TP se fera uniquement à travers l'interface web TPLab. Vous devrez fournir une unique archive .tar contenant les deux fichiers tp4-Roudoudou.l et tp4-Roudoudou.y (où Roudoudou est remplacé par votre nom).
Certaines questions appellent à une réponse que vous pouvez mettre en commentaire dans vos fichiers.
Attention : les points suivants ne rapportent rien, mais ne pas les respecter pourra retrancher jusqu'à 10 points sur la note finale :
-
vos fichiers devront comporter un commentaire au début avec vos nom, prénom, filière et numéro du TP,
-
votre code doit compiler avec les options -Wall -Wno-unused-function -Werror -Wextra -pedantic -std=gnu99 -O3.
Liens utiles
-
l'archive du TP : info502-tp4.tar
-
encadrant de TP : Pierre.Hyvernat@univ-smb.fr
1. Préliminaires
Définir une valeur de type regex
(pour le type défini dans le TP
précédent) est verbeux :
regex test =
cat(
star(symbol('a')),
star(
plus(
plus(
cat(cat(symbol('a'), symbol('a')), symbol('b')),
cat(symbol('b'), cat(star(symbol('b')), symbol('a')))
),
cat(symbol('b'), symbol('b'))
)
)
);
Tester vos fonction du TP précédents sur des regex non triviales était donc un peu lourd.
L'objectif de ce TP est d'introduire la notion de parseur : c'est un programme qui lit du texte (fichier, chaine de caractères, stdin, ...) et le transforme en valeurs utilisable par votre programme. Il sera ainsi possible de définir la regex précédente simplement avec
a* (aab + bb*a + bb)*
Nous allons utiliser Lex et Yacc pour cette tache:
-
Lex permet de faire une analyse lexicale du texte. Son but principal sera de découper le texte en morceaux appelés tokens. La chaine
a* (aab + bb*a + bb)*
deviendra ainsiSYMB(a) STAR LPAR SYMB(a) SYMB(a) SYMB(b) PLUS SYMB(b) SYMB(b) STAR SYMB(a) PLUS SYMB(b) SYMB(b) RPAR STAR
Cette analyse lexicale s'apparente à la décomposition d'une phrase en mots.
-
Yacc permet de faire une analyse grammaticale du résultat. Son but sera d'interpréter les tokens en fonction d'une grammaire. C'est lui qui transformera la suite de tokens en
cat( star(symbol('a')), star( plus( plus( cat(cat(symbol('a'), symbol('a')), symbol('b')), cat(symbol('b'), cat(star(symbol('b')), symbol('a'))) ), cat(symbol('b'), symbol('b')) ) ) )
Nous allons en fait utiliser Flex et Bison pour remplacer Lex et Yacc. Les paquets correspondants pour Ubuntu / Debian sont flex et bison...
La plupart des langages possèdent des outils similaires...
Récupérez l'archive du TP ainsi que votre fichier tp3-Roudoudou.c du TP précédent. (Il resservira...)
Modifiez le Makefile et changez le nom des fichiers tp4-Roudoudou.l tp4-Roudoudou.y (sauf si votre nom est Roudoudou) et testez le programme :
$ make
gcc -Wall -Wno-unused-function -Wextra -pedantic -std=gnu99 -O3 -lgc -c utils.c
gcc -Wall -Wno-unused-function -Wextra -pedantic -std=gnu99 -O3 -lgc -c tp3-Roudoudou.c
yacc -d tp4-Roudoudou.y
conflits: 4 décalage/réduction
gcc -Wall -Wno-unused-function -Wextra -pedantic -std=gnu99 -O3 -lgc -c y.tab.c
lex tp4-Roudoudou.l
gcc -Wall -Wno-unused-function -Wextra -pedantic -std=gnu99 -O3 -lgc -c lex.yy.c
gcc -Wall -Wno-unused-function -Wextra -pedantic -std=gnu99 -O3 -lgc main.c utils.o tp3-Roudoudou.o y.tab.o lex.yy.o -o regex:w
$ ./regex
# a+b.c
add(symbol('a'),cat(symbol('b'),symbol('c')))
#
(Pour sortir, il suffit d'appuyer sur Control-d...)
2. Grammaire
Tokens
Les tokens (càd les mots) sont définis dans le fichier tp4-roudoudou.l. Comme les noms PLUS
,
STAR
, etc. sont déjà utilisés par le type regex
,
j'ai utilisé les noms
-
rePLUS
pour l'addition ("+"), -
reSTAR
pour l'étoile ("*"), -
reZERO
pour l'expression vide ("0"), -
reONE
pour le mot vide ("1"), -
reDOT
pour la concaténation ("."). -
reSYMB
pour un symbole.
Les autres tokens définis sont :
-
END
pour la fin du fichier, -
NL
pour un saut de ligne, -
LPAR
etRPAR
pour les parenthèses, ouvrantes et fermantes.
Règles de grammaire
Le fichier Yacc tp4-Roudoudou.y contient la grammaire utilisée. La règle initiale est spécifiée avec
%start commandes
%start
est un mot clé Yacc, et commandes
est le
symbole non-terminal de la grammaire par lequel on commence.
La fonction (appelée yyparse
) essaiera donc de repérer des
chaines dans le langage des commandes
.
Les grammaires sont données dans une syntaxe à la BNF, mais avec des
actions possibles, données entre accolades. On commence avec la
grammaire toplevel
(ceci est indiqué dans le fichier par la ligne
%start toplevel
):
toplevel:
END { printf("\n"); exit(0); }
| line toplevel
Autrement dit, le toplevel
accepte soit :
-
le token
END
(qui est généré à la fin du fichier), auquel cas l'action affiche simplement un saut de ligne et quitte le programme, -
une ligne (définie par la grammaire
line
) suivie dutoplevel
. Dans ce cas, aucune action n'est donnée. Il y aura par contre des actions associées aux règles de la grammaireline
.)
Les lines
sont simplement des commandes (cmd
)
suivies d'un retour chariot (token NL
, pour "newline"), et les
commandes sont générées par la grammaire:
cmd:
{ printf("?"); }
| expr { print_C_regex($1); }
Autrement dit, une commande est soit :
-
vide, auquel cas on affiche simplement un point d'interrogation,
-
une expression (
expr
est un nouveau symbole non-terminal de la grammaire). Dans ce cas, l'action est :-
on appelle la fonction
print_C_regex
, -
on passe à la ligne,
-
on affiche le nouveau prompt.
Le point important est l'argument de la fonction
print_C_regex
:$1
est remplacé par la valeur obtenue par le symbole non-terminalexpr
de la règle. Ce symbole est le premier de la règle et s'appelle donc$1
. Le deuxième s'appellerait$2
, etc.Il faut donc que les symboles non-terminaux puissent avoir une valeur ! Le type des valeurs associées au symbole non-terminal
expr
est donné par la ligne%type <regex> expr
Les symboles non-terminaux
toplevel
etline
n'avaient pas de valeur. Le symbole non-terminalexpr
en a. La valeur associée à une règle est donnée en définissant la "variable"$$
. Ainsi, la règleexpr: ... | expr rePLUS expr { $$ = plus($1, $3); } ...
-
signifie qu'une expression peut être générée par :
-
une expression (dont la valeur sera dénoté par
$1
) -
le token
rePLUS
-
une expression (dont la valeur sera dénoté par
$3
)
L'expression résultat sera obtenu en calculant le résultat de la fonction
plus
appliquée aux deux expressions $1
et
$3
.
La grammaire fournie (dans le fichier tp4-Roudoudou.y
) ne
reconnait pas les regex contenant des étoiles:
# a+b.c*
syntax error
add(symbol('a'),cat(symbol('b'),symbol('c')))
Ajoutez cette possibilité en ajoutant une règle dans la grammaire des
expressions pour accepter le token reSTAR
.
N'oubliez pas de vérifier que cela fonctionne, en testant par exemple la regex a+b.c*.
3. Associativité, priorités ...
Il y a deux manières de lire a+b+c:
-
add(symbol('a'),add(symbol('b'),symbol('c')))
-
add(add(symbol('a'),symbol('b')),symbol('c'))
La grammaire donnée est donc ambigue. Yacc le détecte car il affiche un message conflits: 4 décalage/réduction lors de la compilation.
Il est possible de préciser laquelle des deux possibilités on souhaite
utiliser en déclarant le token rePLUS
(correspondant au symbole
"+") avec
%left rePLUS
ou
%right rePLUS
dans le fichier tp4-Roudoudou.y, par exemple après les déclarations des tokens
%token reZero ...
%token ...
Testez ce qui se passe avec la ligne
%right rePLUS reDOT
puis avec la ligne
%left rePLUS reDOT
Comment sont interprétées les expressions
-
a+b*.c
-
a.b*+c
Pour gérer les priorités des opérations +, . et *, il est possible de donner plusieurs lignes
%left
ou %right
. La priorité d'une opération
dépendra de son ordre d'apparition. Par exemple,
%left rePLUS
%left reDOT
Mettez les contraintes d'associativités sur plusieurs lignes (et ajoutez en une pour l'étoile) pour gérer les priorités. N'oubliez pas de tester pour vérifier que vous n'avez pas mis les priorités à l'envers.
4. Synomymes
Ajoutez un token pour le symbole "?" et une règle pour pouvoir interpréter le point d'interrogation comme dans les regex POSIX.
Pour ajouter un nouveau token QUESTION
, il faut :
-
renvoyez
QUESTION
pour le caractère "?" dans le fichier tp4-Roudoudou.l, -
déclarez le token
QUESTION
dans le fichier tp4-Roudoudou.y avec le mot clé%token
.
Bonus, (à faire en fin de TP) ajoutez des tokens et des règles pour pouvoir interpréter la syntaxe POSIX "R{13,27}" pour "au moins 13 répétitions, au plus 27 répétitions". Attention, il faudra ajouter des tokens pour lire des nombres entiers et écrire des fonctions auxiliaires.
5. Nouvelles commandes
Remplacez l'appel à la fonction print_C_regex
par un appel à la
fonction print_regex
afin d'avoir un affichage plus "user
friendly".
Ajoutez ensuite une règle aux expressions pour appeler la dérivé:
$ ./regex
# (a.b.c)* / "ab"
c(abc)*
Il faudra pour ceci ajouter un token pour le symbole "/".
Le token STR
est déjà défini et renvoie une valeur de type
char*
.
Pour le moment, la seule commande (grammaire cmd
) possible est
d'afficher une expression.
Ajoutez un token IN
et une règle dans la grammaire des commandes
(cmd
) pour permettre de tester l'appartenance d'une chaine au
langage d'une regex.
$ ./regex
# "abc" IN a.b*.c
TRUE
# "abc" IN c.b*.a
FALSE
Si vous n'avez pas programmé la fonction match
, ou si votre
fonction derivative
du TP2 ne fonctionne pas, vous pouvez ajouter
une commande pour tester la fonction contains_epsilon
à la
place...
6. Concaténation implicite
Ajoutez une règle pour permettre la concaténation implicite: on pourra ainsi écrire "abc" au lieu de "a.b.c".
Que pensez-vous des expressions
-
"ab+c"
-
"a+bc"
Comme la concaténation implicite ne correspond a aucun token, on ne peut pas déclarer de priorité ! Il va donc falloir désambiguer la grammaire à la main.
Modifiez la grammaire en ajoutant des catégories :
-
expr
continue d'exister et correspond aux regex arbitraires -
term
correspond aux regex qui ne commencent pas explicitement par une somme:-
ab + cd* n'est pas reconnu par la grammaire
term
-
a(b+c).d est reconnu par la grammaire
term
, tout comme a(bc*)* ou (ab + cd*), où la somme est cachée par les parenthèses.
-
-
atom
correspond aux regex atomiques : 0, 1, les symboles, les expressions parenthésées, les étoiles, ...
Votre grammaire pour les expressions se décomposera donc en 3:
expr:
...
| ...
| ...
term:
...
| ...
| ...
atom:
...
| ...
| ...
Vérifiez que votre grammaire fonctionne correctement:
$ ./regex
# a+bc
a + bc
# ab+c
ab + c
# abc*
abc*
# a.b*c+d
ab*c + d
# (ab + c)*d+e
(ab + c)*d + e
7. Pour finir (Bonus)
Ajoutez les opérations du TP2 que vous avez écrites :
-
empty
pour tester si une regex est vide, -
infinite
pour tester si une regex a un langage infini, -
reverse
pour calculer le "renversé" d'une regex, -
prefix
pour calculer la regex des préfixes d'une regex, -
suffix
pour calculer la regex des suffixes d'une regex, -
subword
pour calculer la regex des sous-mots d'une regex, -
...
Faites la partie bonus de la question 6.
Ajoutez une commande pour afficher un message d'aide.
...