Compléments d'information
Je me propose d'ajouter dans cette page, des informations et des
réponses aux questions qui m'ont été posées relativement au projet de
programmation d'intégration formelle. J'indiquerais par mail et en
tête de page les ajouts ou modifications successives. Pour l'instant,
la page est crée le 17 mars.
1 Analyse Syntaxique
L'objet de ce projet n'est pas l'ecriture de l'analyseur syntaxique,
mais ce dernier est relativement indispensable. Voici donc quelques
indications sur comment en implanter un de manière (à mon avis) aussi
simple que possible.
Ce qui suit est inspiré du
TD3
de Luc Maranget.
Vous pouvez également vous inspirer du code du corrigé de son
TD-9,
en particulier pour l'analyse lexicale.
1.1 Analyse lexicale
Au début il n'y a même pas de verbe.
On part d'une suite de caractère (soit une chaîne de caractère -ou
string, soit d'un flux -ou stream). L'analyse lexicale
consiste a reconnaître les mots (ou lexème), et à transformer
l'input brut en suite de lexèmes.
A priori, vous avez besoin des lexèmes sin, cos, (, ), +, *, -,
x, etc.
1.2 shift et reduce
Il faut ensuite faire une analyse syntaxique. C'est-à-dire transformer
la suite de lexèmes en arbre syntaxique.
On parcourt la suite de lexèmes. A chaque étape, on a le choix entre
deux opérations, shift et reduce.
-
shift consiste juste à empiler le lexème courant sur une
pile stackL (avec L comme lexème)
- reduce consiste à transformer le haut de la pile stackL
en arbre syntaxique et à pousser celui-ci sur une pile stackT
(avec T comme tree).
L'idée est que l'on suppose l'expression analysée suffisemment
parenthésée pour éviter de se poser des questions par rapport aux
précédences entre opérations. On sait alors que l'on doit faire reduce lorsque l'on tombe sur une parenthèse fermante, si l'on tombe
sur un opérateur sans argument (comme x) ou bien s'il
reste un opérateur (comme * ou sin) en tête de stackL après
un reduce. Par exemple pour
sin(2*x+3)
on partira de
sin((2*x)+3)
et le parsing se déroule alors comme suit. Rappellez-vous bien que
stackL est une liste de lexème, alors que stackT est une
liste d'expressions déja analysées.
prochaine lexème |
stackL |
stackT |
opération |
sin |
[] |
[] |
s |
( |
[sin] |
[] |
s |
( |
[sin; (] |
[] |
s |
2 |
[sin; (; (] |
[] |
r |
* |
[sin; ( ; ( ] |
[2] |
s |
x |
[sin; ( ; (; * ] |
[2] |
r |
) |
[sin; ( ;; *] |
[2; x] |
r |
+ |
[sin; ( ] |
[2*x] |
s |
3 |
[sin; ( ; + ] |
[2*x] |
r |
) |
[sin; ( ; + ] |
[2*x; 3] |
r |
) |
[sin; ( ] |
[(2*x)+3] |
r |
|
[] |
[sin((2*x)+3)] |
|
On peut encore simplifier l'analyse en supposant qu'il y ait encore
plus de parenthèses. On ne fait alors qu'un seul reduce à la
fois, et seulement quand on ferme une parenthèse. Mais il faudrait
alors partir d'une expression comme
(sin(((2)*(x))+(3))).
1.3 Ajouter des parenthèses pour les précédences
Une astuce pour gérer les précédences entre opérations est de
parcourir une fois l'expression après l'analyse lexicale et avant
l'analyse syntaxique en rajoutant des parenthèses. On fait comme ceci:
-
On commence par ouvrir trois parenthèses.
- On remplace ``+'' (ou ``-'') par ``))+((''.
- On remplace ``*'' par ``)*''.
- On ferme trois parenthèses à la fin.
On peut ensuite utiliser l'algo précédant sans danger.
1.4 L'opposé et la soustraction
Il reste un point délicat. Le signe ``-'' peut être utilisé soit pour
désigner la soustraction, soit pour désigner l'opposé. Dans le premier
cas, il s'agit d'un opérateur binaire, dans le second cas, d'un
opérateur unaire.
En fait, ``-'' doit être compris comme un opérateur unaire si et
seulement si
-
il apparait en début d'expression,
- ou bien s'il apparait après une parenthése ouvrante.
Une solution simple consiste donc à parcourir l'expression après
l'analyse lexicale, et à remplacer les ``-'' qui correspondent au cas
unaire par un autre lexème comme ``--''. On a alors complètement
désambiguée l'expression pour permettre une analyse lexicale facile.
2 A propos de la commutativité
Cette question a été posée par Florin Cremenescu. Il fait remarquer
que a+b+a ne se réécrit pas naturellement en 2*a (à supposer que
vous vouliez considérer des facteurs entiers). En fait le problème se
pose aussi pour simplifier a+b-a par exemple.
Comme je l'indiquais dans le sujet, il est difficile de traiter
l'associativité-commutativité. Je suggère donc de ne pas trop passer de
temps là-dessus, surtout dans un premier temps (et on ne vous en
voudra donc pas beaucoup si votre programme ``rate'' quelques
simplifications).
Cela-dit, on peut essayer de se débrouiller à peu près avec des moyens
simples. Ce qui suit est une suggestion pour ratrapper un peu le
coup. Mais encore une fois, traiter ce problème dans toute sa
généralité est difficile; sachez ne pas être trop difficiles ou trop
exigeant avec vous-même.
Un premier point est d'orienter l'associativité, par exemple
les expressions ci-dessus peuvent être toutes lues comme a+(b+a), et
plus géneralement comme a+(b+(c+(d+...))) par exemple.
Ensuite, on peut essayer de retrouver les termes identiques dans une
somme en orientant la régle de commutativité. Vous pouvez pour cela
définir une fonction de ``mesure'' (en fait plutôt une fonction de
hash ou hachage, voir le poly) h sur vos expressions. Une fois
que vos expressions sont normalisées par rapport aux règles usuelles,
on peut alors appliquer des transformations comme
a+(b+c) ® b+(a+c) si h(a)>h(b)
ce qui revient à ordonner les termes de la somme par rapport à la
valeur que leur attribue h. Les membres idientiques de la somme
doivent alors se retrouver ensembles.
Il y a sans doute d'autres possibilités. Mais encore une fois, faites
attention à ne pas passer trop de temps sur ce genre de problèmes, au
moins au début du travail.
3 A propos de l'unification (10 avril)
Je rajoute quelques remarques à propos de l'unification; j'en porfite
pour vous donner un peu de vocabulaire et répondre à une question de
Franck Guo et Mathieu Perrin.
L'opération d'unification se fait toujours pour un langage
donné. C'est-à-dire un certain nombre de symboles de
fonctions. Dans notre cas, ces symboles sont sin, cos,
+, 0, etc. Chacun de ces symboles à une arité; c'est le nombre
de ses arguments. Par exemple, + est d'arité 2.
Remarquez que les constantes sont des fonctions d'arité 0: 0 et 1 sont
des constantes d'arité 0.
Pour faire de l'unification, on travaille sur des expressions
construites à partir des symboles de fonctions et de variables. Une variable est une expression (encore)
inderterminée. L'unification consiste essentiellement à résoudre une
équation entre deux expressions avec des variables.
Le résultat de l'opération d'unification est soit:
-
l'échec,
- une liste de valeurs des variables utilisées pour lesquelles
l'équation admet une solution.
L'unification échoue pour sin+yº cos.
Pour sin+yºsin+cos, elle réussit, et retourne [y¬
cos].
Pour sin+yº x+cos, elle réussit et retourne [y¬cos;
x¬ sin].
Pour x+xºsin+sin, elle réussit et retourne [x¬
sin].
Pour x+xºsin+cos, elle échoue.
On peut remarquer que certains de ces exemples sont ``plus simples''
que d'autres. Plus exactement, voici deux cas particuliers:
définition 1
On appelle filtrage (ou pattern-matching) l'opération
d'unification dans le cas ou seul un des deux membres de l'équation
contient des variables.
Ci-dessus, seule la troisième équation ne rentre pas dans le cas
particulier du filtrage.
définition 2
On appelle motif (ou pattern) un membre d'une équation,
c'est-à-dire une exporession qui peut contenir des variables.
définition 3
Un motif linéaire est un motif où chaque variable n'apparait au
plus qu'une fois.
On remarque ici que seul le motif x+x des deux dernières équations
n'est pas linéaire.
L'unification est un tout petit peu plus facile à programmer dans le
cas linéaire. Un exemple de régle non linéaire est typiquement:
a+a |® 2*a
ou plus généralement:
a+n*a |
|® |
(n+1)*a |
n*a+ m*a |
|® |
(n+m)*a . |
Si vous voulez effectivement programmer de telles règles, notez
qu'alors le ``+'' dans n+1 ou n+m désigne vraiment l'opération d'addition et pas le symbole de fonction correspondant.
This document was translated from LATEX by HEVEA.