• Aucun résultat trouvé

Contributions

Dans le document Réécriture et compilation de confiance (Page 163-168)

6. Stratégies de réécriture réflexives en Java 113

7.1. Contributions

7.1.1. Contraintes dans la compilation du filtrage

Le filtrage non-linéaire est une particularité des motifs de Tom. Il est absent des

langages de motifs de la plupart des langages fonctionnels commeML ou Haskell.

Les filtres non-linéaires sont particulièrement utiles lorsqu’il s’agit d’explorer des listes,

comme on a déjà pu le voir dans les exemples 7 et 8 dans le chapitre 2, en permettant

d’éliminer un grand nombre de filtres inutiles produits par le filtrage de liste. La

tech-nique utilisée par le compilateurTométait de rendre le motif linéaire, en attribuant aux

différentes occurrences de la variable répétée des noms frais. L’égalité de ces variables une

fois instanciées par filtrage est alors vérifiée avant d’exécuter l’action. Le motif présenté

en figure 7.1a est compilé de manière similaire au motif de la figure 7.1b.

Cette technique a l’avantage d’être simple : les motifs sont linéarisés, et il est alors

possible d’utiliser l’algorithme de compilation du filtrage linéaire pour compiler un motif

non-linéaire. Cependant, le code alors généré est inefficace, car tous les filtres pour le

motif linéarisé sont explorés complètement. Cela est particulièrement sensible lorsque le

%match(List l) {

conc(_*,x,_*,x,_*) -> {

// find a

// duplicated "x"

print(‘x);

}

}

(a) Recherche de doublons non-linéaire

%match(List l) {

conc(_*,x,_*,y,_*) -> {

if(‘x.equals(‘y)) {

print(‘x);

}

}

}

(b) Recherche de doublons avec test

Fig.7.1: Recherche de doublons par filtrage non-linéaire, et équivalent utilisant un test

motif comporte de nombreuses sous-listes ainsi qu’une variable répétée un grand nombre

de fois.

Exemple 38. Le filtrage de liste de Tom permet de manipuler les chaînes de

carac-tères comme des listes. Une utilisation courante du filtrage de listes sur les séquences

de caractères est le découpage d’une ligne en plusieurs sous-chaînes en fonction d’un

caractère particulier. On peut ainsi vouloir explorer les différentes entrées d’un fichier

/etc/passwd, qui est constitué de lignes contenant 7 entrées séparées par le

carac-tère «:». L’opérateur de liste associé à la sorte String étant unique, il n’y a pas

d’ambiguïté, et le nom de l’opérateur peut être omis dans l’écriture de filtre, qui peut

alors être écrit comme :

%match(String entry) {

(login*,x,pass*,x,uid*,x,gid*,x,name*,x,path*,x,shell*) -> {

if(’:’ == ‘x) {

// some action

}

}

}

Ce motif cherche 7 champs séparés par un même caractère, et exécute l’action si ce

caractère est bien «:».

Le filtre de l’exemple 38 lorsqu’il est compilé en utilisant la linéarisation du filtre,

introduisant alors 5 variables fraîches, et effectuant un test vérifiant que les valeurs

attribuées par filtrage à chacune d’elles sont égales avant d’exécuter l’action, produit

un algorithme de filtrage très inefficace. En effet, tous les découpages de la chaîne de

caractères d’entrée sont explorés entièrement.

On introduit afin de compiler ces filtres de manière efficace une notion de contrainte

associée aux variables. Ainsi, à chacune des variables fraîches introduites par la

linéarisa-tion du motif est associé une contrainte d’égalité avec la première variablex. La

compi-lation de l’algorithme de filtrage est alors faite en tenant compte de ces contraintes : dès

7.1. Contributions

qu’une quantité suffisante d’information est disponible au cours du processus de filtrage,

il est vérifié si la contrainte est satisfaite. Ceci permet de ne pas continuer le découpage

de la chaîne lorsque le début du découpage n’est pas valide, c’est à dire qu’un champ a

été délimité par des caractères différents.

La figure 7.2 illustre le temps d’exécution du motif de l’exemple 38 avec le schéma de

compilation sans contraintes, puis en considérant les contraintes d’égalité au niveau des

variables. La découpe d’une seule entrée du fichier/etc/passwd, par exemple :

postfix:*:27:27:Postfix User:/var/spool/postfix:/usr/bin/false

prend environ10 secondes dans le premier cas, pour un temps inférieur à1milliseconde

dans le second cas, lorsqu’elle est exécutée sur une machine équipée d’un processeur

Pentium 4 cadencé à 3 gigahertz.

Avec contraintes Sans contraintes 0 5 10 15 20 0 50 100 150 200

Nombre d’entr´ees Exploration de/etc/passwd

Secondes

Fig. 7.2: Tests du filtrage associatif non-linéaire avec et sans contraintes

La gestion et la propagation des contraintes d’égalité sur les variables au sein de

l’algorithme de filtrage permet d’obtenir des gains d’efficacité substantiels pour les motifs

utilisant plusieurs variables non linéaires.

Cependant, le motif de l’exemple 38 peut être rendu plus efficace en considérant non

seulement une variable non-linéairex, mais directement la constante «:» au sein du

mo-tif, comme une constante algébrique. Les premières versions du compilateur ne pouvaient

pas effectuer une telle optimisation.

On étend alors la notion de contrainte gérée par le compilateur des contraintes d’égalité

entre variables à des contraintes plus générales. Parmi elles, on considérera les contraintes

d’égalité d’une variable avec un constante. Le motif de l’exemple 38 peut alors s’écrire

en utilisant la constante ’:’en lieu et place des variables x. Les contraintes d’égalité à

cette constante sont alors générées au sein de l’algorithme de filtrage, permettant de ne

par continuer le découpage de l’entrée lorsque le caractère choisi comme séparateur de

champs n’est pas «:».

Le motif s’écrit alors :

%match(String entry) {

(login*,’:’,pass*,’:’,uid*,’:’,gid*,’:’,name*,’:’,path*,’:’,shell*)->{

// some action

}

}

On compare en figure 7.3 l’efficacité du motif de l’exemple 38 compilé avec l’algorithme

de propagation de contraintes d’égalité de variables avec la version utilisant aussi des

contraintes d’égalité entre variables et constantes. La seconde version s’avère environ

deux fois plus rapide, car le découpage correct est trouvé sans essai de découpages

in-corrects, utilisant6occurrences d’un caractère différent de «:». L’évaluation du second

motif est linéaire en la taille des entrées, tandis que l’efficacité du premier dépend de

la fréquence et de la répartition des caractères dans les entrées ; si il y a beaucoup de

redondance, de nombreux découpages incorrects seront testés.

Contraintes et constantes Contraintes d’´egalit´e 200 400 600 800 1000 1200 0.1 0.2 0.3 0.4 0.5 0.6

Nombre d’entr´ees Exploration de/etc/passwd

Secondes

Fig. 7.3: Tests du filtrage associatif non-linéaire avec et sans gestion des constantes

Filtrage avec contraintes. L’introduction tout d’abord de contraintes d’égalité entre

variables afin de permettre de mieux compiler les motifs non-linéaires, ainsi que leur

pro-pagation au sein de l’algorithme de filtrage compilé lui-même, puis l’ajout de contraintes

d’égalité entre variables et constantes permettent l’obtention de gains en efficacité

im-portants, sur des applications d’utilité commune, comme la manipulation de chaînes de

caractères.

L’implémentation et la conception de ce mécanisme de gestion de contraintes au niveau

de l’algorithme de filtrage ont été faites de manière générique et extensible, fournissant

une base pour l’intégration de filtrage avec des contraintes arbitraires. L’objectif est

7.1. Contributions

de pouvoir compiler efficacement des conditions attachées aux motifs comme autant de

contraintes sur les variables de filtrage.

7.1.2. Extensions de ApiGen

Avant l’introduction deGom,ApiGen était l’un des outils clé du développement avec

Tom, générant les structures de données utilisées pour représenter les AST du

compi-lateur, ainsi que les structures de données des différents exemples. Certaines extensions

du langage de spécification de signatures d’ApiGen ont du être introduites, afin de

permettre l’utilisation plus facile de ces structures.

C’est en partie ces extensions qui ont guidé la conception du langageGom, ainsi que

l’expérience acquise en modifiant le compilateur ApiGen qui a dirigé la conception de

son compilateur.

Symboles de liste nommés

Une première extension a été de permettre la définition des noms des symboles

as-sociatifs. En effet, le langage de spécification original d’ApiGen permet de définir un

opérateur de liste en fonction de son domaine ainsi que de son co-domaine. De plus, il

n’est possible de définir qu’un seul opérateur de liste par co-domaine, et cette sorte ne

peut contenir aucun autre opérateur. Ainsi, la définition d’un opérateur de liste de sorte

List agrégeant des éléments de sorteElement s’écrit :

list(List,Element)

Le nom de l’opérateur de liste associé était alors dérivé automatiquement du nom

du co-domaine, ici concList. Nous avons introduit une déclaration de liste nommée,

permettant de donner un nom à l’opérateur de liste, afin de simplifier l’écriture des motifs

et constructions de termes dans les programmes. La déclaration de la liste précédente

peut alors être :

named-list(conc,List,Element)

Une telle spécification permet alors d’utiliser un nom significatif comme opérateur de

liste. Une liste d’instructions dans l’AST d’un programme sera alors identifiée par le

constructeur Sequenceplutôt qu’un constructeur concInstructionmoins lisible.

Modularité des signatures

Un autre ajout au langage de spécification de signatures d’ApiGen est celui de

mo-dules pour les signatures. Les signatures ApiGen sont en effet contenues dans un seul

fichier, et ne peuvent pas être réutilisées. Elles contiennent une liste de déclarations de

constructeurs et d’opérateurs de listes.

Lorsque l’on doit manipuler une signature de grande taille, comme peut l’être celle

qui décrit les AST d’un compilateur, il est nécessaire d’être capable de distinguer les

différentes parties de la spécification des AST, par exemple en séparant les instructions

du langage de ce qui concerne le typage.

On introduit alors une constructionmodulentryàApiGen. Celle-ci permet de déclarer

un nouveau module, qui éventuellement importe lui même d’autres modules, avec la

liste des sortes qu’il contient, suivie par la liste des déclarations d’opérateurs. Le module

déclarant les sortes et opérateurs implémentant les entiers de Peano peut alors être :

[

modulentry(

name("Peano"),

[],

[type("Nat"),type("List")],

[

constructor(Nat,Zero,Zero()),

constructor(Nat,Suc,Suc(pred<Nat>)),

named-list(conc,List,Nat)

]

)

]

Les importations de modules sont résolues à la compilation, et leur cohérence vérifiée.

Les modules sont alors aplatis avant la compilation. Le code à générer pour chaque

module ne dépend que de la « signature » des modules qu’il importe ou qui sont importés

par l’un de ces modules, c’est-à-dire le nom du module et des sortes qu’il déclare. Il faut

alors calculer la fermeture réflexive et transitive de la relation d’importation de modules

afin de déterminer les modules qui doivent être recompilés lors d’un changement.

Dans le document Réécriture et compilation de confiance (Page 163-168)