Boulot qui reste a faire:
Ends:
The gcc team suggests:
Si on veut generer du ARM a partir du JAVA, le trait rpz un compilateur complet. On rajoute un compilateur de JAVA a MIPS, de JAVA a IA-32, etc. et ce pour tous les compilateurs en entree.
Cette strategie n'est pas bonne car il y a beaucoup de compilateurs et de la duplication de code.
Supposons qu'on aie une representation intermediaire:
Il suffit plus d'un traducteur de la representation intermediaire vers ARM, MIPS, etc. On a plus que 4 traducteurs, on a transforme une multiplication en addition.
Comment definir cette representation intermediaire?
On a besoin d'une representation flexible pour pouvoir etre la cible des langages de hauts niveaux mais assez assembleur pour etre traduit en assembleur.
Affecter un poids aux langages en entree et ceux en sortie: avoir un baricentre
Si tout le mone a un poids de 1, le langage intermediaire sera plutot front-end, sinon plutot backend.
Comment calculer le baricentre de tous mes langages ?
On fait l'instersection de toutes les fonctionnalites des langages.
Qu'est-ce qui se passe le jour ou on rajoute un nouveau langage? Le baricentre va bouger.
Deux langages intermediaires: un pour le front-end, un pour le back-end
Traduction de Java a 1 c'est facile, de 1 a 2 difficile.
On aura plein de petits traducteurs simples et un gros traducteur complique, on a seulement 10 traducteurs.
On rajoute deux nouveaux langages intermediaires:
La traduction de 1 vers 3 et de 4 vers 2 est moins dure que de 1 vers 2.
On est en train de reconstruire une multiplicite de traducteurs.
C'est le desucrage. Dans notre langage intermediaire on veut desucrer notre langage en entier.
Comment definir tous ces langages intermediaires?
Syntax, grammaire, type-checker, etc.
Deux solutions:
Intermediate Representations (IR) are fundamental.
Intermediate representation:
A whole range of expressivities, typically aiming at making some optimizations easier:
What level of machine independence?
On doit construire notre langage en essayant d'etre le plus proche possible du code assembleur tout en etant suffisemment abstrait pour pas faire rentrer le nom des registres
Intermediate-language design is largely an art, not a science.
Muchnink, 1997
float a[20][10]
...
a[i][j+2]
Traduction dans les langages intermediaires:
t1 <- a[i, j+2]
On va avoir besoin de temporaires pour noter les resultats intermediaires.
t1 <- j + 2
t2 <- i * 20
t3 <- t1 + t2
t4 <- 4 * t3
t5 <- addr a
t6 <- t5 + t4
t7 <- *t6
On a une approche progressive plus agreable qu'une approche directe.
r1 <- [fp - 4]
r2 <- r1 + 2
r3 <- [fp - 8]
r4 <- r3 * 20
r5 <- r4 + r2
r6 <- 4 * r5
r7 <- fp - 216
f1 <- [r7 + r6]
Sans la traduction precedente, il est impossible de comprendre comment cette traduction fonctionne.
class Gcd {
static public int gcd(int a, int b) {
while (a != b) {
if ( a > b)
a -= b;
else
b -= a;
}
return a;
}
static public int main(String[] arg) {
return gcd(12, 34)
}
}
Advantages
Disadvantages
Une representation sous forme de registre est fatalement plus verbeux.
Du a:
Chaque fonction utilise un certain nombre de registres, il y a beaucoup de travails supplementaires pour maintenir la coherence des registres.
How is the structure coded ?
Quadruples vs Triples:
i <- i + 1
t1 <- i + 1
t2 <- p + 4
t3 <- *t2
p <- t2
t4 <- t1 < 10
*r <- t3
if t4 goto L1
(1) i + 1
(2) i sto (1)
(3) i + 1
(4) p + 4
(5) *(4)
(6) p sto (4)
(7) (3) < 10
(8) *r sto (5)
(9) if (7), (1)
On note le resultat de chaque ligne dans (nb). Autant ne pas prendre cette strategie et prendre une proche des microprocesserus actuels.
Advantages:
Disadvantages
On a besoin d'un langage intermediaire qui nous sert de support pour le reste du cours
On en train de faire remonter les infos du microprocesseur au langage intermediaire, on doit gerer le minimum possible de la memoire pour rester abstrait
Different kinds of memory in a computer, with different performances:
Use the registers as much as possible
Si notre langage a pas la recursion, on n'a pas de pile a gerer.
What if there are not enough registers ? Use the main memory, but how?
Recursion:
Depending on the persistence, several models:
Data to store on the stack:
Est-ce qu'on peut mettre le content dans l'ordre que je veux ?
On doit decider d'un layout respecte par tous les compilateurs.
The layout is suggested by the constructor.
Au milieu de ces 2 pointers on a notre block d'activation. Lors d'un appel de fonction, on descend fp sur sp puis on descend sp. On doit etre capable de stocker l'ancienne valeur de fp.
auto: Static size, automatic memory
malloc: Dynamic size, persistent memory
Automatic memeory is extremely convenient
int
open2(char* str1, char* str2, int flags, int mode){
char name[strlen(str1) + strlen(str2) + 1];
strcpy(strcpy(name, str1), str2);
return open(name, flags, mode)
}
On est en train de faire un tableau dynamique.
On est sur une variable locale stockee sur la pile, cad stockee sur le bloc d'activation. On doit etre capable de preallouer la zone necessaire. Hors ici on dit que la taille de la zone depend des parametres au runtime et ne sera pas connu lors de la compilation.
Pour une meme fonction f, on peut avoir des blocs d'activation de taille variable. Au moment ou on rentre dans la fonction on doit pousser le stack pointer vers le bas avec alloca:
int
open2(char* str1, char* str2, int flags, int mode){
char *name = (char *) alloca(strlen(str1) + strlen(str2) + 1);
strcpy(strcpy(name, str1), str2);
return open(name, flags, mode)
Une variable est consideree comme une variable non locale si elle est declaree dans une fonction englobante et utilisee dans une autre fonction imbriquee.
let function trace(fn: string, val: int) =
(print(fn); print("("); print_int(val); print(")"))
function one(input : int) =
let function two() =
(trace("two", input); one(input - 1))
in
if input > 0 then
(two(); trace("one", input))
end
in
one(3); print("\n")
end
Resultat de l'execution: