3 janv. 2012

Mini-cours ILP 4

Cours ILP





Cours ILP

Par Stéphane Kouadio


1) Introduction


 
Bienvenue dans ce cours d'ILP. Vu l'énorme quantité d'informations qu'on a à savoir dans cette matière, ce document tentera dans la mesure du possible de résumer ce qu'il faut savoir,et est certainement incomplet. Pour un cours plus complet, se référer au cours de Mr Christian Queinnec. Avant tout, il est important de bien maitriser le langage C et Java avant de poursuivre cette ue (même si j'évoque quelques brefs rappels ;-) ). So let's go !








Qu'est ce que ILP ?



ILP (Implantation d'un Langage de Programmation) est le nom d'une matière enseignée en Master STL de l'UPMC. Comme son nom l'indique, nous implantons un langage de programmation dans cette matière (langage qui s'appelle lui aussi ILP). Ceci afin d'étudier comment fonctionnent les langages de programmation vis à vis du programmeur et de la machine (compilation, exécution, etc... ) .

Pourquoi des langages de programmation ? 

Nous savons que l'ordinateur ne comprends que le langage binaire, encore appelé langage machine. Ainsi, si nous voulons créer un simple programme qui affiche "Bonjour", il va falloir écrire ce "Bonjour" dans le langage de l'ordinateur, donc en langage binaire (composé uniquement de 0 et de 1). Si vous y arrivez, alors chapeau, et bonne nouvelle : vous pouvez arrêter la lecture de ce cours :-). Mais si vous êtes comme moi, alors vous pouvez continuer :-) . Etant donné qu'il est difficile pour nous êtres humains de travailler directement en langage machine, nous avons créé des programmes, appelés "compilateurs", qui jouent le rôle de "traducteur" langage humain ==vers==> langage machine. Ainsi, nous pouvons écrire nos programmes dans le langage qu'on veut, et s'assurer que nous disposons d'un "traducteur" (compilateur) qui traduira en langage compréhensible par l'ordinateur, qui pourra aisément exécuter le programme.
Etant donné le vaste champ de mots possible dans notre langage humain à nous, les langages de programmation utilisent très peu de mots clés, ce qui facilitera le travail du traducteur (compilateur). Par exemple on utilisera le mot spécial "si" au lieu de "si on a".
Remarque: La compilation n'est pas le fait de traduire un langage (lisible par un être humain) en langage machine; mais est un procédé qui consiste à écrire un programme (écrit dans un langage A) en un autre (écris dans un langage B). Notez bien la différence. On pourra donc parler de compilation si on traduit un programme C en programme Java. Ou si on traduit un programme xml en programme C. Dans cette matière donc, nous allons écrire notre nouveau langage ILP qui sera écrit au départ dans un format A, et compilé en un format B.



À quoi ressemble le langage ILP ?
Un programme ILP pourrait ressembler à ceci :

programme HelloWorld {
 afficher "Bonjour Monde" ; 
 afficher "Fin du programme" ; 
}


Ou encore à :

let programme ()= 
 print "Bonjour Monde" ; 
 print "Fin du programme" ;; 


Mais nous avons choisi de l'implanter sous le format XML, afin de faciliter la programmation de l'interpréteur ou du compilateur. ILP ressemblera donc à :

<programme>
 <!-- corps (balises)du programme  ILP ici ...-->
</programme>




Tu as dis "interpréteur".. C'est quoi encore ?

Très bonne question, car ce mot reviendra très souvent en ILP. Un programme interpréteur est un programme qui travaille directement sur le code écrit dans le nouveau langage de programmation. En général il exécute le programme ligne par ligne. Le compilateur lui, lit tout le code, et le transforme en un autre programme dans un autre langage, contrairement à l'interpréteur qui lit directement le programme du nouveau langage et tente de l'évaluer. Par exemple bash (du shell Linux) est un interpréteur : il attends des suites d'instructions (ou commandes), et les évalue.
Grosso modo, un interpréteur (par exemple écrit en C) peut agir de la sorte :

Si je vois le mot clé "afficher", alors je fais un printf.

Si je vois le mot clé "si" , alors je fais un if

Tout ceci est fait ligne par ligne.
Exemple: supposons que notre langage est composé de l'unique mot clé "afficher", et que nous voulons interpréter un exemple de notre nouveau langage, écrit dans un fichier que j'appelle prog.abc (écris en langage abc :) )
Contenu du fichier prog.abc :


afficher "coucou" ; 

Comment agira le programme qui se chargera d'interpréter ce programme ? :
  • 1) Il ouvrira le fichier prog.abc pour le lire
  • 2) tant qu'on est pas arrivé à la fin du fichier
    • Si le mot sur lequel on est est = à "afficher", alors récuperer (dans une variable) l'argument de afficher (qui est coucou ici), et faire printf( )
Le fait d'avoir fait : "printf" lorsqu'on a rencontré le mot "afficher", est ce qu'on appelle l'évaluation. Evaluer un programme, c'est récupérer "la valeur", ou encore "le sens" de ce programme. Ici, le sens du mot clé "afficher" est de faire un printf lorsqu'on voudra traiter ce programme. Par exemple dans ILP, l'évaluation de la balise <entier> signifiera "avoir la valeur de cet entier". On peut raisonner par "évaluer veut dire, qu'est ce que l'interpréteur doit faire lorsqu'il arrivera sur cette ligne ?"
Les choses se compliquent lorsque des fois, nous sommes sur des instructions, dont le sens (ou la définition) est définie à la suite de la ligne sur laquelle on est. Vous me suivez ?
Si on a par exemple :

afficher "hello" ; 
afficher somme(1, 2); 

fonction somme(a,b) {
 retourner a+b ;
}


A la ligne surlignée en jaune, l'interpréteur (qui lit le programme du haut vers le bas) ne connait pas encore la définition de la fonction somme(). Il ne peut donc évaluer directement le programme ligne par ligne. Il est là, obligé de lire une fois le programme, non pas pour l'exécuter, mais pour collecter toutes les fonctions appelées, et les stocker dans des variables (ou structures en C); puis réanalyser le code, et ainsi, lorsqu'on arrivera sur la ligne en jaune, on sait que forcément lors de la première analyse, la fonction a été enregistrée dans une variable (ou structure) de l'interpréteur. Donc ici, le programme interpréteur connait maintenant la fonction, ce qui lui permettra de l'exécuter directement. S'il n'y avait pas eu de première analyse, il serait trop compliqué, voire impossible d'exécuter le programme
Tout ceci est un rappel de comment marche une interprétation d'un programme. Les choses sont légèrement différentes dans ce qu'on fera en ILP.









Au cœur de ILP

Nous avons énoncé plus haut que nos programmes en langage ILP auront la syntaxe XML. Puisque tout nouveau langage doit être accompagné d'interpréteur/compilateur (je rappelle que l'interpréteur "parse" (analyse, ou parcourt) le fichier et l'exécute (l'évalue), tandis que le compilateur lui, parse le fichier, et le réécris dans un langage différent (qui peut être en langage machine, ou un autre langage de programmation)) , il nous faudra écrire l'interpréteur et le compilateur. Dans ILP, l'interpréteur est écrit en Java, le compilateur est lui aussi écris en Java. Le langage "résultat" de la compilation (traduction) est le langage C.
Maintenant que nous savons tout cela, nous allons voir comment est faite proprement dit une exécution d'un programme écris en langage ILP. Avant de continuer, notons ces caractéristiques du langages ILP :
  • ILP est du style de Javascript : les variables n'auront pas de type, le type de la valeur d'une variable sera déterminé dynamiquement (lors de l'évaluation du programme). On pourra donc ecrire (pseudo-code) : maVariable = 4 ; maVariable = "une chaine" ; (maVariable est non typée, et on peut lui mettre n'importe quelle valeur autorisée dans le langage)
  • ILP est du style des langages fonctionnels : lors de l'évaluation d'une séquence d'instructions, c'est la dernière valeur de la séquence qui est considérée. Par exemple si on est dans une fonction, il n' y a pas d'instruction "return", car on retournera la dernière instruction. Si une fonction veut renvoyer 2, elle fera (code style ML) :

let func = 
 // calcul ...
 // calcul ...
 // dernière instruction
 2
;;



Je vous recommande de lire le cours officiel, en complément de celui-ci, pour connaitre les détails de la manière d'écrire des programmes ILP (balises, etc... ).

Comment exécuter un programme ILP ?
1) D'abord le programmeur écrit son programme ILP dans un fichier , qu'il pourra donner pour extension .xml . Note: pour connaitre les balises, voir le fichier grammar1.rnc (en TME);
<!-- monprog.xml -->

<program1>
 <entier valeur='4' />
</program1>




2) Notre programme Java veut l'interpréter. Pour cela, nous disposons d'une classe qui se chargera de lire le fichire XML, et de le transformer
en DOM. Pour ceux qui ne savent pas, DOM (Document Object Model) est une norme de répresentation des noeuds XML, pour faciliter leur manipulation.
Par exemple la transformation en DOM du programme xml ci-dessus donnera le schéma suivant :









Regardez bien ce schéma. Lorsqu'on transforme notre document XML en DOM, le premier noeud de l'arborescence est un
noeud spécial, appelé Document. C'est le noeud "racine", qui nous permettra d'accéder
aux autres noeuds du programme xml. Ensuite vient le noeud "program1" , qui contient (comme fils) le noeud "entier".
"entier" n'a pas de fils, mais un attibut, de nom "valeur". Tous les "rectangles" que j'ai ici représenté sont
appelés "Node" en DOM, ou encore sont de type "Node".


En Java, la classe Node est une classe abstraite (car ça ne précise pas si
c'est un noeud "balise", ou un noeud "attribut", un noeud "texte", ... ). Pour spécifier cette classe Node,
on dispose de plusieurs sous classes, dont la classe "Element", (en anglais Element = balise ), qui indique
que le noeud est une balise. Element hérite de Node.

Si vous avez donc bien compris, dans cette partie, on transforme le code xml en DOM, pour le manipuler plus facilement
dans nos programmes Java.



Ayant transformé notre programme xml en DOM, nous manipulerons donc ici des éléments (objets)
de type "Node", ou encore "Element" si nous sommes sur des balises. Le problème est que un objet de type Element (balise) ne
nous dit rien sur le type de cette balise au sein de notre programme ILP. On ne sait pas si on est sur une
balise qui signifiera "un nombre entier" , ou "une chaine de caractère", ou "une fonction à appeler", etc... On peut décider par exemple de demander à chaque fois le nom de la balise, pour savoir sur quel noeud on travaille; le problème est que cela réduirait fortement les performances, d'autant plus lorsqu'on fera des calculs récursifs sur les noeuds, ou des fois lorsqu'on a besoin d'analyser plusieurs fois un fichier xml avant de le traiter. La solution qu'on a adoptée est alors la transformation du DOM, en AST.


AST ?

Rassurez-vous, AST (Abstract Syntax Tree) n'est pas quelque chose de compliqué. C'est juste la transformation du DOM (obtenu précédemment) en "objets" qui sont aussi reliés entre eux comme dans l'arbre du DOM, mais qui ont un sens beaucoup plus clair et donc sont plus simples à manipuler. Par exemple avec notre programme xml ci-dessus, on pourra avoir un noeud (ou un objet) "Entier", qui contiendra donc toutes les méthodes pour la manipulation des entiers au sein du programme. Sans l'AST, on serait obligé de tester si le nom de la balise était = à "entier", puis s'il a un attribut valeur, etc...
Grâce à la réprésentation du DOM en AST, on peut très bien lire le code d'un programme de manière arborescente.
Une réprésentation en AST de mon programme xml ci-dessus donnerait :








Ici, si on est sur le noeud AST "Entier", on peut lui demander directement sa valeur (avec une méthode par exemple nommée getValeurEntier() ) . C'est beaucoup plus simple que dans le DOM où on aurait demandé la valeur de l'attribut "valeur", puis de le caster en entier (car il était probalement de type String en DOM)... Bref, maintenant, on parle AST, on oublie DOM :).
Gardez dans l'esprit que AST concerne les objets (qui ont été simplifiés pour une manipulation aisée) qu'on va manipuler au sein de ILP. C'est une ré-écriture des balises XML en objets , dont les types (Entier, ... ) nous indiquent déjà de la nature de l'élément sur lequel on est.

Exécution du programme ILP
Après avoir transformé en AST, on a maintenant le choix : soit on désire interpréter le programme, soit on désire le compiler (et après exécuter le programme compilé) . Comme mentionné plus haut, l'interprétation et la compilation sont deux choses différentes. Si on veut interpréter (évaluer) le programme, on peut appeler une méthode (eval() par exemple) à partir de la racine de l'AST, et qui sera rappelée sur chaque noeud (objet) AST du programme. Exécuter un programme, c'est exécuter chaque partie du programme.
En ilp, la fonction qui évalue (exécute) un programme s'appelle eval() . Elle peut être appelée sur chaque noeud (objet) AST du programme.
Dans la première version du langage (ILP1) , le package Java fr.upmc.ilp.ilp1.eval contient toutes les classes de l'AST qui sont évaluables. Vous remarquerez qu'elles commencent toutes par EAST* (pour Evaluable AST ).










(*) Chaque objet EAST* réprésente bien un noeud spécifique dans la structure du programme ILP. Par exemple EASTentier est la classe qui réprésentera un entier dans le programme. L'évaluation d'un tel objet doit renvoyer la valeur de cet entier.
(*) La classe EASTParser est la classe qui gère la transformation du fichier xml en DOM, et DOM vers AST. Pour connaitre le rôle de chaque classe, il vous est inévitable de lire le code de ces classes.
(*) Un bloc unaire est la réprésentation d'un bloc de code dans lequel on initialise une seule variable, qui aura un sens seulement dans ce bloc dans lequel elle est définie. (Un bloc est un groupement (défini explicitement) dans lequel on pourra définir des instructions, etc...).

Comme { /* instruction ... */ } en Java/C (accolades ouvrantes et fermantes).
Exemple de bloc unaire (style ML ):



// avant définition du bloc unaire ...

let nom = "stephane" in
 print_string "La valeur du nom est" ; 
 print_string nom ;;

// apres définition du bloc unaire ...


Ici, nom est une variable du bloc unaire, uniquement visible dans le corps de ce bloc. Après ce bloc, toute mention de la variable nom serait incorrecte. Un exemple de bloc en Java :

for(int i = 0; .... )
{
 // i visible seulement ici, dans ce bloc
}
// i invisible ici 




ILP2
ILP2 est ILP1 auquel on a rajouté des fonctionnalités, telles que la boucle, des bloc-n-aires (un bloc n-aire est un bloc où l'on de définit non plus qu'une seule variable, mais plusieurs variables, qui seront seulement visibles dans le bloc), la possibilité d'affecter des valeurs à des variables, la possibilité de définir nos propres fonctions dans le code, et la possibilité de compiler le programme ILP (en C).
Comme ILP est du style de Javascript, pas besoin de déclarer une variable avant de l'affecter.
Dorénavant, les éléments de l'ast ne sont plus précédés par EAST (Evaluables AST) comme en ILP1, mais par CEAST (Compilables & Evaluables AST), ce qui signifiera ici (juste par convention :-) ) que nos objets qui réprésenteront le programme pourront être exécutés (évalués) comme avant, et compilés, c'est à dire qu'on pourra les ré-érire dans un autre langage. Ici le langage destination de la compilation (ou encore traduction, comme je l'ai mentionné au début) est le langage C. On pourra par exemple dire que l'objet CEASTentier pourra être compilé (ou traduit) en int en C.
Toutefois un problème se pose (eh oui encore) : les objets en ILP n'ont pas de type, tandis que lorsqu'on devra les traduire en C, il va bien falloir leur donner un type, puisque C est un langage dont les variables sont typées. De plus, l'héritage n'existe pas en C, on ne peut donc pas faire de type générique, dont les éléments vont hériter et spécifier.

Le fichier ilp.c et ilp.h
Pour solutionner ce problème, le prof a choisi de créer un type unique (en C) pour tous les éléments ILP : ILP_Object. Un ILP_Object est un pointeur sur une structure qui contient un champ qui indique le type de cette variable, puis un champ de type union qui contiendra la valeur de cette variable en fonction de son type. Je rappelle qu'une union est un type spécial tel qu'une seule valeur de ce type sera prise en compte parmis toutes celles qui sont listées. Exemple :

struct maStruct {
 union 
 {
  char  french  ; 
  int   english ;
  void* german  ;
 
 } langue ;    
} objet ; 

// Dans tout le programme entier, on ne pourra faire que : // soit objet.langue.french // soit objet.langue.english // soit objet.langue.german // Mais pas les 3 écritures en même temps. // Une fois qu'on utilise un champs de l'union, les autres disparaissent.

Tous les éléments ILP seront donc compilés avec le type pointeur ILP_Object.


ILP 3
ILP3 = ILP2 + les exceptions. Le programme ILP est maintenant capable de déclencher des exceptions au moyen d'instructions spéciales (throw). Une exception au sens ILP peut contenir n'importe quelle valeur ILP. Imaginez par exemple un throw ( expression_ILP ) . expression_ILP peut être un entier, une chaine, etc..
Maintenant que le programmeur ILP est capable d'écrire des instructions pour manipuler les exceptions, il nous faut l'implémenter aussi pour notre interpréteur Java et notre langage destination de la compilation.
Pour l'interprétation (en Java), ce n'est pas bien compliqué, car les exceptions existent en natif en Java, on peut donc dire que dès qu'on rencontre un try/catch/finally , throw en ILP, on fait réellement le try/catch/finally , throw de Java. Aussi, étant donné qu'on peut avoir des programmes ILP qui ne font que throw (déclenchent des exceptions), il va bien falloir des rattraper lorsqu'on exécutera le programme, au risque de faire planter l'interpréteur ILP avec une exception non rattrapée. Ainsi donc, tout programme ILP est équivalent à :

try 
{
 executer le gros programme ILP ici...
}
catch(Exception e)
{
 // Le programme ILP a déclenché une exception non rattrapée au sein du même programme.
 // On rattrape cette execption ici donc.
 
}




ILP4



ILP4 est la version suivant ILP3, à laquelle on a rajouté l'intégration (inlining). Si vous ne savez plus de quoi parlais ILP3, voici donc un bref listing des évolutions du langage :
  • ILP1: création de variables (sans affectation), utilisation de fonctions primitives, bloc unaire, alternative (if-then-else)
  • ILP2: ILP1 + bloc local (ou bloc n-aire), boucle, affectation de variables, possibilité de définir ses propres fonctions
  • ILP3: ILP2 + Exceptions (try-catch-finally + throw)
  • ILP4: ILP3 + intégration

Qu'est-ce que l'intégration ?
Lors du développement d'un langage de programmation, il est important de ne pas négliger le facteur "vitesse d'exécution". Notre objectif ici est donc d'optimiser le code, afin de gagner en temps d'exécution. Voici un exemple d'optimisation : par exemple, si un programme contient :

print 5 + (2 + 3) ; 

Sans optimisation, l'ordinateur aurait exécuté - comme on le lui a demandé- la somme de 2 et 3; puis aurais additionné ce résultat à 5, avant de mettre ce dernier résultat en argument à la fonction "print". Tout cette gymnastique peut être optimisée en écrivant tout simplement :

print 10 ; // code précédent optimisé 

Notons que ce code optimisé est la "ré-écriture" du programme précédent, avant que l'ordinateur ne l'exécute. Cette réécriture, ou analyse est donc faite de manière statique. Avant de parler d'intégration, il est bien de savoir distinguer ce qui est statique de ce qui ne l'est pas (dynamique).

Statique vs Dynamique 

On nomme statique TOUT ce qui peut être traité avant l'exécution du programme. Par exemple dans mon programme précédent, la valeur de l'argument de print est bien connue avant l'exécution du programme. Le compilateur peut donc se permettre d'optimiser le code, en écrivant une valeur plus compacte de l'argument ( 10 au lieu de 5+(2+3)) pour gagner en temps.
Tout ce que le compilateur ne peut connaitre avant l'exécution du programme est appelé dynamique.

Par exemple, dans ce programme (pseudo code): si on a :


var n ;
saisirAuClavier(n);
afficher (n + 18 );


Ici l'argument de afficher() ne peut pas être optimisé par le compilateur, car il ne connait pas la valeur de n avant l'exécution du programme. On ne sait pas quel nombre sera entré au clavier. On est donc ici dans un cas de calcul dynamique, et non statique. Si vous avez bien compris cela, on peut continuer l'inlining (intégration), et fermer cette parenthèse ;-)


Intégration
L'intégration est une autre technique d' optimisation, qui consiste en fait à "intégrer" le code d'une fonction dans la ligne où elle est invoquée. Par exemple si avant on avait :

g = 1
function somme(x,y) {
     return x + y + g; 
}
 
var a = 1, b = 2 ;
var c = somme(a, b) ;


L'intégration serait qu'on incorporerait le code de somme là où elle a été invoquée. On aurait donc :


g = 1 ;
function somme(x,y) {
    return x + y + g; 
}

var a = 1, b = 2 ; 
var c = ({ var x = a; var y = b; return x + y + g });

Comment ça marche ? : On a ouvert une parenthèse pour indiquer qu'on va écrire une expression. L'accolade indique ici une suite d'instructions. Dans cette suite d'instructions, on a remplacé les arguments de somme() par les valeurs passées en paramètre. C'est pourquoi on y déclare deux variables locales au bloc, x et y, qui auront pour valeur a et b.
Les choses se compliquent lorsque la fonction déclare/affecte aussi des variables. Le problème est qu'il peut avoir des conflits de nom de variables entre celles déclarées dans la fonction, et celles déclarées hors de la fonction. Supposons par exemple que notre programme (hors fonction) déclare une variable locale appelée g, en plus de la variable globale g existante. Lors de l'incorporation du corps de la fonctionà la ligne où elle est appelée, on aura :
var c1 = ({ var x2 = a1; var y2 = b2; return x2 + y2 + g}); . Le "g" ici ne parlera plus de la variable globale g, mais de la variable locale g déclarée, ce qui donnera forcément un résultat incohérent.
Une substitution "naïve" pourrait donc avoir des effets inattendus (par exemple parler de la variable locale g au lieu de la variable globale g). Pour plus de détails sur les conflits, je vous reporte au transparent de cours, qui évoque les différentes incohérences qu'on peut rencontrer en se contentant de juste "remplacer betement" les variables qui sont dans le corps de la fonction à intégrer (incorporer).
La solution qu'on a choisi pour corriger ce problème est ce que l'on nomme l'alpha-conversion .

L'alpha-conversion

Dans cette technique, on décide de renommer systématiquement seulement TOUTES les variables locales, de sorte à ne pas avoir de conflit de nom avec les variables globales. Ainsi, lors de la substitution, les variables globales, ayant leur nom inchangé, conserveront leur sens dans le bloc de la fonction.
Exemple: Le programme précédent devient :


g = 1 ; // Variable globale
function somme(x1, y1) {
    return x1 + y1 + g; 
}

var a1 = 1, b1 = 2 ; 
var c1 = ({ var x2 = a1; var y2 = b2; return x2 + y2 + g});



Remarque: Dans mon exemple, la partie en rouge désigne une expression. On a écrit "(" suivi de "{" . La parenthèse indique qu'on attend une expression. En écrivant des instructions (celles de la fonction) là où sont attendus des expressions (comme le code écris en rouge), nous cassons une règle utilisée dans les précédentes versions de ILP : avant, on avait bien une expression qui pouvait "être" (au sens héritage) une instruction, maintenant on a une instruction qui "est" aussi une expression, puisqu' utilisée là où on s'attendait à des expressions (comme le code en rouge dans l'exemple). On ressent donc ici le besoin d'un double héritage : une instruction "est" une CEASTinstruction et "est" une CEASTexpression. Comme le double héritage n'existe pas en Java, on utilise une autre technique qu'on appelle la délégation (expliqué à la fin du cours) .

Etapes pour l'intégration

Comme nous l'avons mentionné, avant d'intégrer, il est important de renommer les variables locales, pour éviter les conflits de noms. C'est le principe de l'alpha-conversion. Ci-dessous, nous pouvons voir le code de la méthode prepare() de la classe Process, classe utilisée pour préparer nos nos programmes ILP à être testés . (Cette classe est le point d'entrée avant toute analyse, en ILP) .




D'abord, nous récupérons le DOM à partir du fichier XML avec la méthode getDocument(). Puis, on crée un AST à partir de ce DOM avec la méthode getParser().parse(d) . Cet AST renvoyé est attribué comme l'AST du programme ILP, avec setCEAST().
Ensuite, nous appelons performNormalization() qui fera la normatlisation de l'AST, et renverra un nouvel AST. Nous remplaçons donc l'ancien AST par le nouvel AST réduit, dont les variables locales sont renommées ...
Après cela, nous voulons maintenant optimiser notre code (en procédant à l'intégration des fonctions là où elles sont appelées). Mais en ILP, nous nous sommes interdits d'intégrer les fonctions qui sont récursives. C'est pourquoi il faut avant tout vérifier quelles sont les fonctions récursives, et quelles ne le sont pas. C'est ce que fait la méthode computeInvokedFunctions() . On dit aussi qu'elle calcule le graphe d'appel des fonctions : elle remplit une table de hachage telle que, à une fonction f(), est associée une liste de fonctions que la fonction f() appelle.
A partir de là on peut maintenant intégrer, avec la méthode inline().
Cette méthode parcourt l'AST : dès qu'elle arrive sur une invocation de fonction, si la fonction à invoquer n'est pas récursive, alors on va l'intégrer. En fait l'intégration est implémentée de cette manière : on rajoute dans chaque nœud AST, un champ qui s'appelle inline, de type bloc local (souvenez-vous de mon exemple de code intégré en rouge). Ce champ contiendra le code "intégré" de la fonction. En n'écrasant pas l'ancienne version de l'AST, mais en gardant la nouvelle version dans un champ de classe, cela nous permet, non seulement d'avoir les deux versions, mais aussi de pouvoir les comparer, et savoir si l'on gagne (en vitesse).
.Après avoir intégré, comme auparavant, on collecte les variables globales, ce qui nous servira pour la compilation vers le langage C (en C il faut prédéclarer les variables globales avant de les utiliser, ce qui n'est pas le cas en ILP. D'où la raison de cette collecte, avant compilation en C).


Récapitulatif des fonctions, et ordre

1) L'utilisateur écrit son programme ILP
2) Nous disposons de la classe Process, qui se charge de préparer le programme ILP à être analysé. Dans cette préparation, il nous est nécessaire de créer toutes les structures de données, qui vont réprésenter le code du programme ILP, et qui nous seront utiles dans la manipulation des éléments du programme : cette structure de données est appelée AST. Pour cela, nous transformons d'abord le XML en DOM.
3) On parse (analyse) le document DOM, et on crée un AST à partir de ce DOM
4) On normalise cet AST. Je rappelle que la normalisation a pour but de transformer l'ancien AST en un nouvel AST, tel que dans ce nouvel AST, on ait fait l'alpha-conversion (suppression des conflits de noms de variables en effectuant un renommage). En réalité, en ILP, on ne renomme pas les variables, mais on joue sur leurs références physiques en mémoire. On sait que l'adresse d'un objet mémoire est unique, donc pour tester si deux variables sont égales, on a juste à tester si les deux on la même adresse, au lieu de tester si les deux ont le même nom :

if(var1 == var2) // test d'égalité physique. C'est ce qu'on fait en ILP, au lieu de if(var1.getName().equals(var2.getName))
5) On intègre

Aucun commentaire:

Enregistrer un commentaire