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 STARCette 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
-
rePLUSpour l'addition ("+"), -
reSTARpour l'étoile ("*"), -
reZEROpour l'expression vide ("0"), -
reONEpour le mot vide ("1"), -
reDOTpour la concaténation ("."). -
reSYMBpour un symbole.
Les autres tokens définis sont :
-
ENDpour la fin du fichier, -
NLpour un saut de ligne, -
LPARetRPARpour 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 (
exprest 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:$1est remplacé par la valeur obtenue par le symbole non-terminalexprde 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
exprest donné par la ligne%type <regex> exprLes symboles non-terminaux
topleveletlinen'avaient pas de valeur. Le symbole non-terminalexpren 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
QUESTIONpour le caractère "?" dans le fichier tp4-Roudoudou.l, -
déclarez le token
QUESTIONdans 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 :
-
exprcontinue d'exister et correspond aux regex arbitraires -
termcorrespond 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.
-
-
atomcorrespond 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 :
-
emptypour tester si une regex est vide, -
infinitepour tester si une regex a un langage infini, -
reversepour calculer le "renversé" d'une regex, -
prefixpour calculer la regex des préfixes d'une regex, -
suffixpour calculer la regex des suffixes d'une regex, -
subwordpour 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.
...

