:: Enseignements :: Master :: M2 :: 2011-2012 :: Machine Virtuelle (et bazard autour ...) ::
![[LOGO]](http://igm.univ-mlv.fr/ens/resources/mlv.png) | Lab 3 |
Le lab3 consiste à prend un sujet parmi les sujets proposés que vous devrez effectuer tout seul.
Le sujet devra être traité et rendu sous forme d'une archive au format zip (tout rar, tar, 7z etc. ne sera même pas ouvert)
à votre nom de famille à envoyé avant le 25 février 23h59.
L'archive devra comprendre le code source un README, un fichier ant (build.xml), ainsi qu'un rapport
de 8/10 pages au format PDF, expliquant avec vos mots, le sujet, l'approche choisie, les résultats obtenus,
les solutions écartées, les problème rencontrés, les bugs connus ainsi qu'un ensemble (au moins 20) scripts de tests.
Les sujets sont réparties en 3 catégories, compilateur, runtime et mixed (compilateur+runtime)
en fonction des parties de code qui doivent être modifiées et chaque sujet possède un niveau estimé
(facile, moyen, dure).
Compilateur
Cette section regroupe les sujets ayant attrait à la compilation uniquement.
Throw (niveau facile)
Le but est d'implanter l'instruction
throw expr dans le langage permettant
de jeter une exception ayant comme message la valeur de l'expression.
Il faut pour cela modifier la grammaire pour introduire l'instruction
throw
puis modifier le typechecker et générateur de code.
On ne vous demande pas d'ajouter un
catch !
Inférence de type (niveau moyen)
Le but est d'améliorer la façon dont le compilateur calcul des types
des variables.
Actuellement, le type est calculé en fonction de la déclaration fait
par l'utilisateur et non en fonction du contexte.
Par exemple, le code suivant infère que
a est un
any
alors qu'il pourrait être inférer comme étant un
int.
var a = 2
De plus, l'alorithme devra considérer que chaque affectation crée une nouvelle variable.
Dans le code ci-dessous, le premier print doit être typé
print(int) et
le second
print(double).
var a = 2
print(a)
a = 3.0
print(a)
Dans le cas d'un
if, il faut considérer que si une variable est assigné dans
une des branches, une nouvelle variable est créé après le
if.
var a = 2 // a0 est un int
var b = 3
if (b == 3) {
a = 3.4 // a1 est un double
} // a2 est un any
Dans le cas des boucles, comme on veut garder un algorithme presque linéaire, une variable
déclaré avant la boucle devra être considérer comme une nouvelle variable
si elle est assigné dans le corps de la boucle entre le début du corps de la boucle
et l'endroit où elle est modifiée. Elle sera considérer comme une autre variable
de l'endroit où elle est assigné jusqu'à une nouvelle assignation ou la fin de la boucle.
Après la boucle, la variable (ou une nouvelle) sera considéré comme
any.
var a = 2
var b = 3
print(a) // print(int)
for(var i = 0; i < 10; i = i + 1) {
print(a) // print(any) car peut être appelé
a = 4.0
print(a) // print(double)
}
print(a) // print(any)
En terme d'implantation cela veut dire que le corps d'une boucle pourra
être typechecké au maximum 2 fois si cela est nécessaire.
Max (niveau facile)
Actuellement, le compilateur utilise la bibliothèque ASM pour calculer
le nombre maximal de variables locales et la taille maximale de la pile
qui sont des informations obligatoires dans le bytecode.
L'idée est de ne plus utiliser la bibliothèque ASM pour cela (elle restera
utilisée pour la génération proprement dite) et de calculer ces valeurs
lors de l'étape de génération.
section 3 of
ASM guide
StackMap Frame (niveau facile)
Actuellement, le compilateur utilise la bibliothèque ASM pour calculer
les
stackmap frames qui sont des informations obligatoires dans le bytecode
depuis la version 7 de Java.
L'idée est de ne plus utiliser la bibliothèque ASM pour calculer les
stackmap frames.
La bibliothèque restera utilisée pour générer les
stackmap frames proprement dit.
Il vous faudra donc calculer les stack-frames aux endroits ou ceux-ci sont requis
et utiliser l'API d'ASM pour les traduires en bytecode.
Même si ce sujet peut sembler assez similaire au sujet précédent, l'algorithme n'est pas le même !
section 3.1.5 of
ASM guide
Propagation des constantes (niveau facile)
Actuellement le compilateur ne propage pas les constantes, 1 + 2 est compilé
iconst_1
iconst_2
iadd
alors que le compilateur pourrait directement remplacé le code par la valeur 3.
iconst_3
Le but de ce sujet est donc d'implanter la propagation des constantes sur tout
les types (true && false, "foo" + "bar, etc)
ainsi que la suppression de code mort à cause de la propagation des constantes
code par exemple
if (false && true) {
// dead code, should be removed
print("dead code")
}
Linear Scan allocator (niveau dure)
Le but est d'implanter l'algorithme d'allocation de registre linéaire pour
numéroter les variables locales.
Register_allocation
et
Section 4 de
http://www.cs.ucla.edu/~palsberg/course/cs132/linearscan.pdf
Inlining de fonctions (niveau facile)
Le but est lors de la génération d'inliner le code des petites fonctions, par exemple
fun f(x, y) {
return x + y
}
{
print(f(2, 3))
}
devra produire le même code que celui produit par
{
int $x = 2
int $y = 3
print($x + $y)
}
L'algorithme devra donc differencier les fonctions suivant leurs tailles (en nombre
d'instructions) puis remplacer le code lors de la génération.
L'algorithme devra être capable d'inliner avec une profondeur maximale supérieur à 1
et ne devra pas inliner les fonctions récursives.
Notez que la phase de typage devra aussi être modifier légèrement car
dans l'exemple ci-dessus
x et
y sont typés
any
alors que
$x et
$y sont typé
int.
Visiteur avec des method handles (niveau facile/moyen)
Un des problème des interpreters/compilateurs et autre typecheckers est que lorsque
l'on essaye d'optimizer leurs vitesse ceux-ci utilisent énormément le mécanisme de
double-dispatch des visiteurs hors chacun des
dispatch est
megamorphic donc non-inlinable par la VM.
Le but est ici d'éviter le
double-dispatch;
Il est possible de simplifier le double-dispatch pour ne faire qu'un seul dispatch
car il est possible de stocker l'association entre le noeud de l'AST et la méthode à exécuter
(sous forme de MethodHandle) dans une table de hachage.
Dans le cas d'un visiteur sur l'AST nous connaissons l'ensemble des types pour
lequels le visiteur peut être appelé, il s'agit de tout les noeuds de l'AST, il est donc possible
de précalculer la table de hachage en parcourant la classe
Visitor, en récupérant
le type du premier paramètre de toutes les méthodes publiques (pas
protected).
Puis, pour chaque type (c'est en fait toujours une classe),
de trouver parmis les méthodes déclarés du visiteur courant quelle est celle qui doit être appelée.
Par exemple, la classe
Visitor définie les types ExprAdd, ExprSub, etc, pour chaque classe
il est possible de trouver la méthode
visit de la classe du
SimpleInterpreter
correspondant.
Une fois l'association crée, il suffira lors de l'appel à la méthode
dispatch d'appeler
la méthode correspondant à la classe du premier argument et utilisant
invokeExact.
Optionellement (dans ce cas le niveau de ce sujet devient moyen)
il est possible de faire une implantation lazy (paresseuse)
du visiteur avec une table de hachage qui au lieu de calculer quelles méthodes doient être appelées pour
quels types lors de la construction du visiteur, va faire ce caclcul lors de l'appel à
dispatch.
Pour que le code soit efficace, le résultat de l'association devra être stocker dans la table de hachage
ce qui va poser un gros problème de concurrence car si deux threads utilise deux visiteurs différents
mais de la même classe, il devront utiliser la même table donc il rique d'y avoir des mutations
de la table de façon concurrente.
Il n'est pas souhaitable d'utiliser ici une structure de donnée concurrente comme la
ConcurrentHashMap car celle-ci est basé sur des verrous et le coût d'acquisition d'un verrou
va être supérieur au coût de faire le
double-dispatch.
L'astuce consiste à faire en sorte que les entrées dans la table de hachage soit pré-réservées de
telle façon que la structure la table de hachage ne puisse pas être modifiée par des appels concurrents.
Extraction de boucle (niveau facile/moyen)
Les optimisateurs de code optimise les fonctions et les boucles. Une étapes simple qui peut être faite
avec de réèlement optimiser consiste à transformer les boucles en appel de fonction pour simplier
l'écriture de l'optimisateur de code.
Le but de ce sujet est de transformer les boucles en appels de fonction.
Par exemple,
{
var int a = 0;
var i = 0;
for(; i < 10; i = i + 1) {
a = a + i
}
print a
}
devra être transformé en un code équivalent (car la transfomation se fait à la génération) à
fun Box2 loop$0(int a, i) {
for(; i < 10; i = i + 1) {
a = a + i
}
return new Box2(a, i)
}
{
var int a = 0;
var i = 0;
box = loop$0(a, i)
a = box.get0()
i = box.get1()
print a
}
avec Box2 une classe prédéfinie qui est construit avec deux paramètres.
Il vous faudra donc coder les classes Box1, Box2, ..., Box10, etc ainsi que la classe BoxN
basée sur un tableau dans le cas ou plus de 10 variables sont utilisées dans la boucle.
Optionnellement (dans ce cas le niveau devient moyen), au lieu de pré-coder les classes
Box1,
BoxN, celles-ci peuvent être génerées par le runtime à la demande
en utilisant un
invokedynamic à la place du
new qui créera les classes
si néccessaire.
Suppression de variables non-utilisées (niveau facile)
Le but est de ne pas générer de code dans le cas où une variable est déclarée
mais pas utilisée. Par exemple, dans le code ci-dessous,
b n'est pas utilisé.
var a = 0
var b = 3
print(a)
L'algorithme devra aussi supprimer les variables utilisées mais qui dépendent uniquement
de variable non-utilisé et de constantes (et d'opérations sans effet de bord).
Par exemple, dans le code ci-dessous,
b et
a sont inutilisées.
var a = 0
var b = a + 3
L'algorithme consiste à partir de la fin des instructions et de remonté vers le début,
on note les usages des variables si on voit la assignation (ou la déclaration)
de la variable alors qu'elle n'a pas été utilisé, on peut la supprimer.
Dans ce cas, l'expression de l'assignation (ou de la déclaration) doit être supprimer,
pour notre exemple, comme
b n'est pas utilisé, on ne doit pas généré le
a + 3
donc
a est aussi non utilisé.
Pour les branches des
if (ou pour les boucles), l'utilisation de la variable dans
l'une des branches est suffisant.
Runtime
Cette section regroupe les sujets effectuant des modifications sur le runtime uniquement.
Détection de code chaud (niveau facile)
Le but est d'outiller le runtime pour afficher si une fonction est chaude ou pas.
Exactement, il y a 4 niveaux possibles, 'never called' si la fonction n'a jamais
été appelée, 'called once' si la fonction a été apellée 1 fois, 'warm' si la fonction
a été appelé 1000 fois et 'hot' si la fonction a été appelée plus de 1500 fois.
Le runtime devra fonctionner de la façon suivante, lors de l'exécution d'un script,
le runtime doit enregistrer le nombre d'appel à chaque fonction puis à la fin du
script, le runtime doit afficher un tableau (en ASCII) indiquant la chaudité (hotness)
de chaque fonction. Ces informations sont donc spécifiques à une exécution donnée.
Mixed
Cette section regoupe les sujets pour lesquels il faut faire des modifications
et sur le compilateur et sur le runtime
fonction comme des lambdas (niveau moyen)
Le but est de pouvoir stocker des fonctions dans des variables et d'appeler celles-ci.
Le code suivant devrait donc fonctionner.
fun identity(x) {
return x
}
{
var foo = identity
print(foo) // function: any identity(any)
print(foo(3)) // 3
}
Pour cela, il faut considérer les fonctions déclarées comme étant des variables
possible, une fonction déclaré à postériori doit être visible dans le corps d'une fonction
déclaré à priori.
Lors de l'exécution, 'identity' doit correspondre à une objet de la classe
Function
(à vous de voir la relation avec la classe
fr.umlv.foolang.compiler.Function).
Un appel de fonction sur une variable (ici sur 'foo'), devra appeler la fonction
stocker dans la variable.
lambda (niveau moyen)
Le but est d'implanter la syntaxe des lambdas dans le langage. Une lambda est une fonction anonyme
définie soit par une expression (séparé par une flèche '->') soit par un block.
Le code suivant doit fonctionner.
{
var foo = fun (x, y) -> x + y
print(foo.call(2, 3)) // 5
var bar = fun (x, y) { return x + y }
print(bar.call(2, 3)) // 5
}
En terme de code, le code de création de la lambda est généré en utilisant
invokedynamic
et en passant en argument à la methode de bootstrap le noeud de l'AST correspondant à la
définition du lambda.
Lors de l'appel à la méthode de bootstrap de invokedynamic, le code du lambda est compilé
sous forme de
MéthodeHandle puis stocker dans un objet de type
Lambda.
Lors de l'appel en utilisant
call on vérifie que l'objet courant est de type
Lambda et si c'est la cas, on appel sont méthode handle.
Si l'objet n'est pas de type
Lambda, il faudra générer une exception.
appel de paramètre nommé (niveau facile)
Le but est que l'on puisse nommé les paramètres lors de l'appel
fun foo(a, b) {
print(a)
print(b)
}
{
foo(1, 2) // print 1 and 2
foo(b=1, a=2) // print 2 and 1
foo(1, b=3) // print 1 and 3
foo(a=2, 3) // doesn't compile, named parameter should be at the end
}
La grammaire de
foo accepte déjà les paramètres nommés, il faut juste implanter
le
TypeChecker et le générateur de code. Lors de l'appel, il faut passer le nom
des paramètres si ceux-ci ont un nom en tant que argument de la méthode de bootstrap.
A l'exécution, il faut utiliser la méthode
MethodHandles.permuteArguments
pour remettre les paramètres dans l'ordre avant l'appel.
Typedef et objet (niveau dure)
Le but est d'augmenter le langage
foo pour permettre de déclarer des objets.
On se propose d'ajouter une syntax permettant de définir à un utilisateur de définir
ses propres types.
typedef List { element, List next }
{
var list = List { element: 1, List { element: 2, null}}
for(var l = list; l != null; l = l . next) {
print(l.element)
}
print(list.foo) // error: foo is not defined
list.foo = 3 // error: foo is not defined
}
'typedef' permet de définir un type utilisateur. On peut définir des types à n'importe quel endroit
où on peut définir des fonctions. Il est possible d'utiliser un type avant sa déclaration
avec un typedef.
La syntaxe
List { element: 1, next: null } permet d'allouer une instance d'un type
l'ordre entre les pair champs/valeur n'a pas d'importance.
La syntax avec le '.', par exemple,
l.element, permet d'accéder à un champs.
De plus, on souhaite pour des questions de performance que les classes définient par
l'utilisateur soient représentées par des classes Java et que celle-ci soit créer non pas
lors d'un typedef mais la première fois que la classe est instanciée.
Objet Javascript (niveau moyen)
Le but est d'augmenter le langage
foo pour permettre de déclarer des objets définies
par l'utilisateur. On se propose de réutiliser la syntax/sémantique de Javascript pour cela.
{
var list = { element: 1, { element: 2, null}}
for(var l = list; l != null; l = l . next) {
print(l.element)
}
}
print(list.foo) // error: foo is not defined
list.foo = 3 // ok, now list as a field foo
print(list.foo) // 3
La syntaxe
{ element: 1, next: null } permet d'allouer une instance d'un type
sans nom. L'ordre entre les pair champs/valeur n'a pas d'importance.
La syntax avec le '.', par exemple,
l.element, permet d'accéder à un champs
ou de modifier celui-ci. Si le champs n'existe pas pour l'instance, le champs sera créer.
L'implantation se fait en représentant les objets par des tables de hachage.
homoiconic (niveau dure)
L'idée consiste à permettre à un utilisateur de manipuler l'AST à travers
le langage.
Par exemple,
fun foo(x, y) {
return x + y
}
fun printFoo(x, y) {
print(foo(x, y))
}
{
printFoo(3, 4) // compile printFoo and foo, print 7
f = reflect foo // drop compiled code of foo
f.body.instr.get(0).expr.expr = reflect 2 // replace x by 2 in return x + y
printFoo(3, 4) // re-compile foo, print 6
}
L'instruction 'reflect' suivi d'un nom d'une fonction permet d'obtenir l'AST de la fonction.
L'instruction 'reflect' suivi d'une expression permet d'obtenir l'AST de cette expression.
Si il y a une ambiguité car il existe une fonction foo et une variable foo le typechecker
devra lever une erreur.
A l'exécution, l'instruction 'reflect' charge l'AST, l'AST est unique et toutes modifications
de l'AST se répercute sur l'ensemble du programme.
L'instruction
'x.property' permet d'appeler la Java
x.getProperty()
et l'instruction
x.property = value permet d'appeler la fonction Java
x.setProperty(value).
L'instruction
x.foo(args) permet d'appeler la méthode Java
x.foo(args).
Pour simplifier la sémantique, reflect avec un nom de fonction considère que la fonction si elle a déjà
été compilé, le code doit être jeter. Le prochain appel à la fonction recompilera celle-ci avec
le modification de l'AST si il y a eut entre temps. Si la fonction a déjà été compilée et que
l'on modifie l'AST sans appeler refect sur la fonction, alors le code n'est pas modifié.
duck typing (niveau facile)
On souhaite ajouter la possibilité de faire de la surchage (
overloading) de fonction
mais sur le premier paramètre uniquement, les autres devant être identique.
L'idée est de pouvoir spécialiser un appel en fonction du premier paramètre, comme
pour l'appel virtuelle mais sans définir d'objet (en fait comme en Modula).
fun foo(x) { // generic function foo
print(x)
}
fun foo(int x) { // specilized version for int
print("integer " + x)
}
fun bar(x) {
foo(x) // virtual call (bi-morphic)
}
{
var a = 2.0
bar(a); // print 2.0
a = 2
bar(a); // print integer 2
}
L'appel de fonction peut alors être polymorphe. L'implantation doit utiliser un
inlining-cache
(avec autant de
guardWithTest que vous voulez) mais uniquement dans le cas ou la fonction
est surchargé. Sinon, l'appel de fonction doit se faire avec un site d'appel constant.
le sujet 42 (niveau trop facile)
C'est le sujet qui vous donne tout les points juste pour savoir si vous lisez les docs jusqu'au bout.
Désolé ... c'est une blague.
© Université de Marne-la-Vallée