Nous allons réaliser un transpileur, ou compilateur source à source, pour un sous-ensemble du langage Java, le langage
MiniJava. La représentation d’entrée pour notre compilateur sera donc le langage MiniJava. La représentation en sortie
de notre compilateur sera le langage C. Une exemple de programme permettant de calculer une factorielle
est donné ci-dessous.
classFactorial {
publicstaticvoidmain(String[] a) {
System.out.println(new Fac().computeFac(10));
}
}
classFac {
publicintcomputeFac(int num) {
int numAux;
if (num < 1) numAux = 1;
else numAux = num * (this.computeFac(num-1));
return numAux;
}
}
Comme Minijava est un sous-ensemble de Java, nous pourrons compiler nos fichiers MiniJava en utilisant le compilateur Java javac ce qui sera pratique pour tester la validité
de nos traductions en langage C. En effet, nous pourrons comparer la sortie du programme obtenu en utilisant le compilateur Java, avec la sortie obtenue par l’exécutable obtenu
grâce à notre transpileur.
Vue d’ensemble du transpileur
Nous allons considérer le programme MiniJava suivant pour illustrer cette section.
La figure suivante montre les différentes étapes permettant de passer du fichier source en MiniJava au fichier source transpilé en C.
graph LR;
A[source en MiniJava] -->|caractères| B(fa:fa-tools Analyse lexicale)
B -->|unités lexicales| C(fa:fa-tools Analyse syntaxique)
C -->|arbre syntaxique abstrait| D(fa:fa-tools Analyse de types)
D -->|arbre syntaxique abstrait| E(fa:fa-tools Générateur de code C)
E -->|caractères| F[source en C]
Nous allons maintenant décrire les différentes étapes de la figure ci-dessus. Pour suivre les démos dans les différentes vidéos qui vont suivre, commencer par installer les dépendances comme indiqué ici.
Télécharger ensuite le code en faisant
git clone --recurse-submodules https://github.com/lascar-pacagi/MiniJava.git
cd MiniJava
git checkout v1.0
make
La branche master est la version avec le ramasse miettes et le tag v1.0 est une version sans ramasse miettes.
Si vous voulez apporter des modifications à la version 1.0, vous
pouvez créer une nouvelle branche (from_v1.0 par exemple) en faisant
git checkout -b from_v1.0 v1.0
Le code que je vais utiliser pendant les démos se trouve ci-dessous.
La première étape est l’analyse lexicale qui va permettre de découper un flot de caractères en mots. Ces mots sont appelés unités lexicales. On obtient alors une information
plus structurée où les mots clés du langage, les identifiants, les entiers et les booléens ont été identifiés. Cette phase va aussi nous permettre de supprimer les commentaires et
les blancs (espaces et retours à la ligne). Pour le programme ci-dessus, l’analyse lexicale va produire le flot d’unités lexicales suivantes. On peut voir, par exemple, que le mot clé CLASS
a été identifié, que la constante entière INT_CONST 35 aussi.
CLASS
IDENT ‘Print42‘
LBRACE
PUBLIC
STATIC
VOID
MAIN
LPAREN
STRING
LBRACKET
RBRACKET
IDENT ‘a‘
RPAREN
LBRACE
SYSO
LPAREN
INT_CONST ‘35‘
PLUS
INT_CONST ‘2‘
TIMES
INT_CONST ‘3‘
PLUS
INT_CONST ‘1‘
RPAREN
SEMICOLON
RBRACE
RBRACE
EOF
Dans la vidéo suivante, nous allons donner une vue d’ensemble du transpileur, et nous présenterons l’analyseur lexical de MiniJava.
Analyse syntaxique
La deuxième étape, l’analyse syntaxique, prend en entrée le flot d’unités lexicales et va construire un arbre syntaxique abstrait permettant de représenter la structure du
programme sous la forme d’un arbre. Pour le programme ci-dessus, on obtient l’arbre suivant. On peut y voir, par exemple, l’expression arithmétique avec de manière explicite la priorité
des opérateurs (plus c’est bas dans l’arbre et plus c’est prioritaire).
L’analyse syntaxique a besoin de connaître la structure d’un programme MiniJava.
Cette structure est donnée sous la forme d’une grammaire.
La grammaire de MiniJava, dans sa forme EBNF
est donnée ci-dessous. Une version sûrement plus lisible pour nous en diagramme syntaxique est donnée
ici.^[La description de la grammaire qui permet de générer le diagramme ne suit pas strictement la forme EBNF. Les détails sont donnés ici.]
La vidéo suivante va présenter l’analyseur syntaxique de MiniJava.
Typage
La troisième étape, l’analyse de types, va prendre en entrée l’arbre abstrait et va vérifier si le typage est correct. Par exemple, on va vérifier qu’on utilise les méthodes
avec le bon nombre de paramètres, que les opérateurs + et * sont utilisés avec des opérandes entières, qu’une classe est compatible avec une autre via la relation d’héritage, …
La vidéo suivante va présenter une vue d’ensemble du typage dans MiniJava.
Génération de code
La dernière étape va consister à générer le code en C. On va de nouveau parcourir l’arbre syntaxique abstrait pour générer ce code.
Pour notre example, on obtient le fichier C ci-dessous.
La vidéo suivante va décrire succinctement la génération de code C dans MiniJava.
Sémantique de MiniJava
La sémantique de MiniJava est donnée par sa sémantique en tant que programme Java ^[Voir ici pour la sémantique du langage Java.]. Les principales restrictions sont
Les classes n’héritent pas de la classe Object.
Le mot clé super n’existe pas.
Il y a simplement un constructeur par défaut.
Les seuls types autorisés sont,
int.
boolean.
int[].
Les classes définies par l’utilisateur.
La surcharge d’opérateurs n’est pas autorisée.
L’instruction System.out.printl() ne peut imprimer que des entiers.
Toutes les méthodes doivent retourner une valeur.
Il n’y a pas d’interface, d’exception, de généricité, de lambda.
Nous expliquons dans la vidéo suivante les principales différences entre MiniJava et Java.
Nous avons décrit rapidement l’initialisation par défaut en Java (et MiniJava) dans la vidéo. Nous apportons quelques précisions ci-dessous.
Vous pouvez regarder ici pour tous les détails.
Si on le compile, on obtient les messages d’erreur suivants.
$ javac Init.java
Init.java:6: error: variable i might not have been initialized
System.out.println(i + " " + b + " " + a);
^
Init.java:6: error: variable b might not have been initialized
System.out.println(i + " " + b + " " + a);
^
Init.java:6: error: variable a might not have been initialized
System.out.println(i + " " + b + " " + a);
^
3 errors
En effet, les trois variables i, b et a sont situées sur la pile, et leurs valeurs
vont dépendre des valeurs qui sont situées sur la pile lors de l’appel du main. Java n’initialisant
pas les variables locales avec une valeur par défaut, le compilateur refuse le programme en nous indiquant
que l’on cherche à utiliser des variables non initialisées.
Par contre, le programme suivant est correct car les attributs ont des valeurs par défaut.
Nous allons faire quelques rappels sur la liaison dynamique en Java dans la vidéo ci-dessous, car la gestion
de la liaison dynamique sera un des points difficiles à gérer dans notre transpileur.
Ce code ne compile pas. En effet, à la ligne 9, la méthode m1 est une redéfinition de la
méthode m1 de la ligne 2 : elle à le même nom et les mêmes paramètres. Par contre, pour être
une redéfinition correcte, il aurait fallu que le type de retour boolean soit compatible avec
le type de retour int, mais ce n’est pas le cas.
Ce code ne compile pas. En effet, le type de retour ne permet pas de différentier deux méthodes. Donc
même si les méthodes aux lignes 2 et 6 ont des types de retour différents, comme elles se nomment pareil et ont les
mêmes paramètres, on a pas une surcharge et il y a donc une erreur.
Soit le programme Java suivant.
1classA {
2publicintm1(A a) {
3 System.out.println("int A:m1(A a)");
4return 0;
5 }
6publicbooleanm1(B b) {
7 System.out.println("boolean A:m1(B b)");
8returnfalse;
9 }
10}
1112classBextends A {
13}
Ce code compile-t-il ?
Ce code compile. Cette fois-ci, la méthode à la ligne 6 est bien une surcharge de la méthode
à la ligne 2 car le paramètre est d’un type différent.