Mini-JS-machine : codes expliqués

Fragment 1

Exemple

Code à compiler :

foo = 2; Pour (i = 10; i >=0; i=i-1) { foo = i * 3; } Ecrire (foo);

Code compilé :

CstRe 2 SetVar foo # foo= 2; explication 1.1 CstRe 10 SetVar i # i = 10 GetVar i # debut cond. For CstRe 0 GreEqR # i >= 10 ConJmp 9 # Jump vers fin For GetVar i CstRe 3 MultRe # i * 3; explication 1.2 SetVar foo GetVar i CstRe 1 SubsRe SetVar i # i-- Jump -13 # Jump vers cond. For GetVar foo Print # Ecrire(foo) Halt

Explication 1.1 : Que font SetArg et GetArg ?

La machine dispose d'un environnement/contexte, appelé cc (for "curent context"). Celui-ci associe à chaque variable (ici foo et i) une valeur. La structure des contextes est assez complexe, mais pour l'instant, vous pouvez considérer qu'il s'agit d'une table de hashage qui à chaque nom de variable (String) associe une valeur (flottant ou booléen).

Explication 1.2 : Pourquoi fait-on la multiplication à la fin

Les opérations sur des expressions sont toutes en forme postfix : on calcule les arguments puis on fait l'opération. Cela vaut aussi pour le Set, par exemple. La seule opération qui ne sera pas de cette forme est l'appel de fonction car le nombre d'argument n'est pas fixé. Attention tout de même, l'opération elle même (qui est à la fin) peut prendre plusieurs lignes.

Explication 1.3 : c'est quoi ces nombres à côté du ConJmp et Jump ?

Lorsqu'il est positif, il s'agit du nombre d'instructions à sauter (toujours pour le cas du Jump, et lorsque le booléen récupéré sur la pile est faux pour le ConJmp). Lorsqu'il est négatif, cela veut dire que l'on revient en arrière. Attention,dans ce cas, l'instruction précédente est à -2 pour des raisons d'efficacité.

Fragment 2

Exemple de fonction

Code à compiler :

foo = 2 ; Pour (Var i = 3; i >=1; i=i-1) { Ecrire (bar(i*3)) } Ecrire (foo); Fonction bar (i) { Var x = i - 1 ; foo = x ; Retourne x; }

Code compilé :

DclVar i # explication 2.1, pas obligatoire DclVar bar # pas obligatoire Lambda 26 # explication 2.2.1 DclArg i # explication 2.2.2 SetVar bar # explication 2.2.3 CstRe 2 SetVar foo # foo= 2; CstRe 3 # debut For; SetVar i GetVar i # debut cond. For CstRe 1 GreEqR ConJmp 13 # Jump vers fin For GetVar bar # explication 2.3.1 StCall # explication 2.3.4 GetVar i CstRe 3 MultRe # i * 3 SetArg # explication 2.3.2 Call # explication 2.3.3 Print GetVar i CstRe 1 SubsRe SetVar i # i-- Jump -17 # Jump vers cond. For GetVar foo Print # Ecrire(foo) Jump 9 # explication 2.4.2 # debut de la fonction bar DclVar x # explication 2.1 GetVar i CstRe 1 SubsRe SetVar x # x = i-1 GetVar x SetVar foo # foo = x GetVar x Return # explication 2.4.1 Halt

Explication 2.1 : Variables déclarées

Les variables déclarés avec "Var" sont remontées affin de couvrir tout leur scope : Si elles sont déclarées dans le programme principal, alors elles sont remontées au début, si elles sont déclarées dans une fonction elles sont remontées au début de la fonction.

Attention, seules les déclarations sont remontées, pas les instanciations, ainsi on a toujours le "foo = 2" dans le programme.

Pourquoi fait-on ça ? Affin que les variables soient déclarées correctement. Dans le programme principal ce n'est pas obligatoire car une variable non déclarée est implicitement déclarée dans les variables globales. Dans les fonctions, par contre, c'est essentiel, car la variable x, ici, est déclarée localement dans la fonction, elle n'est pas accessible depuis le programme principal, ni même depuis une autre instanciation de la fonction (on utilise un contexte diffèrent chaque fois).

Explication 2.2 : Déclaration et instanciation de fonction

Au début du programme, nous définissons aussi (et surtout) les fonctions globales. En C, on doit écrire la signature des fonctions avant leur utilisation, mais en JC pas besoin. Pourquoi ? parce qu'une passe de l'interpréteur le fait à notre place. Il en est de même pour notre compilateur, il doit faire quelques étapes pour définir la fonction dans le contexte.

Explication 2.2.1 : Création d'une clôture

Afin de pouvoir renvoyer à la fonction, on fait une clôture : une valeur qui contient un pointeur sur le début du code de la fonction (ici 25 pour dire que c'est 25 lignes après), et un contexte qui est l'environnement dans lequel est définit la fonction (pas important pour le fragment 2).

Explication 2.2.2: déclaration des arguments d'une fonction

On déclare aussi, dès le départ, les noms des arguments de la fonction. Ceci ajoute, dans la clôture, une liste des arguments que demande la fonction. Avant d'appeler la fonction il faudra que l'on donne des valeurs à ces arguments.

Explication 2.2.3: instanciation du nom de la fonction

On fait un dernier SetVar pour associé au nom bar, la clôture que l'on vient de créer. Remarquez que la fonction est rangée de la même manière que les variables, après tout, JS est un langage fonctionnel où les fonctions sont des valeurs comme les autres !

Explication 2.3 : Appel d'une fonction

Pour appeler une fonction, ce n'est pas une étapes, mais plusieurs qui s'articulant autour du calcul des arguments.

Explication 2.3.1: Récupérer la clôture

La fonction est présentée comme une variable, il suffit d'aller dans le contexte chercher la clôture qui lui est associée.

Explication 2.3.2: Instancier les argument

Une fois une fonction sur la pile, nous allons simplement, pour chaque argument, évaluer son résultat sur la pile et l'associé comme argument à la clôture à l'aide de SetArg. Cette instruction associe la valeur de sommet de pile au nom du premier argument de la fonction dans le contexte de la clôture.

Explication 2.3.3: Appel de la fonction

À la fin, nous appelons la fonction qui est au sommet de la pile. Cela sauvegarde le contexte courant et l'endroit où on en est dans le code pour pouvoir y revenir, c'est ce que l'on appel une continuation. Puis, on utilise la clôture pour sauter au code de la fonction et se placer dans son contexte (avec, en particulier, les arguments de la fonctions qui sont maintenant instanciés !)

Explication 2.3.4: Nouveau contexte

Lorsque l'on utilise une fonction récursive, par exemple

Fonction fact (x) { Si (x=0) return 1 Sinon return (fact(x-1)*x); }
lors du second appel, la valeur de l'argument du premier appel n'est pas modifiée : c'est bien une autre variable qui est créé.

Cela signifie que les environnement d'exécutions sont différents pour chaque appel de la fonction ! Affin de formaliser cette différence, on fait appel à StCall, ou StartCall, qui fournie une version fraîche de la clôture,dans laquelle on va pouvoir instancier les arguments, et les variables locales de la fonction sans rien changer aux autres instances.

Remarque : Dans la première version de la correction du TP2, j'avais oublié cette commande. En fait j'avais oublié de la mettre partout (dans la description de la machine et dans son code aussi), j'ai corrigé cela, je m'excuse pour ce bug...

Explication 2.3.5: Variables partagées

Si on crée un nouvel environnement à chaque fois, comment se fait-il que les variables globales (ici foo) ne soient pas modifiées ? C'est parce que l'on crée un environnement partiellement nouveaux : seuls les arguments (et les variables déclarées avec Var) sont dans le nouveau contexte, les autres sont partagé avec le contexte de création de la fonction (et pas celui de l'appel de la fonction !)

Concrètement, un contexte est une liste chaînée de tables de hashage, StCall ne fait que rajouter une table vide en tête de liste, ainsi tout ce qui était disponible avant l'est toujours, mais tout ce que l'on va rajouter en tête ne sera pas visible pour le contexte précédent.

Explication 2.4 : Le code de la fonction

Lorsque l'on accède au code de la fonction, les arguments sont déjà instanciés dans le contexte, on n'a donc qu'à dérouler la compilation du code.

Explication 2.4.1: Le return

Le return est une instruction qui prends le sommet de pile et la première continuation (il jette tout ce qui est entre les deux car c'est probablement de la pollution de pile), puis il restaure le contexte et la ligne de la continuation (on revient donc dans le contexte et le code où on était avant appel de la fonction), tout en gardant le sommet de pile qui est le résultat de la fonction.

Explication 2.4.1: Le jump avant la fonction

En javascript, on peut déclarer une fonction n'importe où, même au milieu de votre code. Il faut donc qu'il y ai un jump avant la fonction qui saute sa définition. Une autre possibilité est de mettre tous les codes de fonctions à la fin.

Fragment 2bis

Exemple d'exception

Code à compiler :

Pour (Var i = 3; i >=0; i=i-1) { Essayer { Ecrire (bar(i*3)) } Rattraper (err) { Ecrire (err);} } Fonction bar (i) { Si (i === 0) Lancer (42); Retourne i; }

Code compilé :

DclVar i # pas obligatoire DclVar bar # pas obligatoire Lambda 32 DclArg i SetVar bar CstRe 10 # debut For SetVar i # i = 10; GetVar i # debut cond. For CstRe 1 GreEqR # i >= 10 ConJmp 19 # Jump vers fin For Continue 21 False # explication 3.2 GetVar bar StCall GetVar i CstRe 3 MultRe SetArg Call Print Drop # explication 3.3 Jump 3 SetVar err # Catch, explication 3.4 GetVar err Print GetVar i CstRe 1 SubsRe SetVar i Jump -23 # Jump vers cond. For Jump 8 # debut de la fonction bar GetVar i CstRe 0 Equal ConJmp 2 CstRe 42 Throw # explication 3.1 GetVar i Return Halt
Explication 3.1: Le Throw

Le Throw renvoi une erreur, pour ça, il va explorer la pile. S'il trouve une continuation d'erreur, il va l'utiliser en laissant le sommet de pile intacte (et en jetant le reste. S'il ne trouve pas de continuation d'erreur, on obtient l'erreur à top-level.

Explication 3.2: Le Try

Le Try, (très mal) appelé "Continue n False" va mettre sur la pile une continuation d'erreur (d'où le Faux...) pointant sur la position fixe n. Si un Throw est lancé c'est cette continuation d'erreur qui sera exécuté, ce qui correspond au Catch.

Contrairement à toutes les autres instruction, celle-ci utilise une position et non un offset...c'est une erreur de ma part qui sera corrigée en introduisant une autre instruction plus pratique (mais en laissant celle-ci).

Explication 3.3: Sauter le Catch

Il y a trois moyens de sortir du Try : ou bien avec un Throw, ou bien avec un Return, ou bien en arrivant à la fin du code. Les deux premiers cas sont automatiquement traités (un peu subtile pour le return, mais ça marche). Pour le dernier cas, il faut ressortir sans faire le Catch, d'où le jump. Il faut aussi enlever la continuation d'erreur de la pile, d'où le Drop.

Explication 3.4: argument du throw

Il Faut, au début de Catch, récupérer l'argument du Throw, qui est en sommet de pile.

Exemple d'exception avec finally (optionel)

Code à compiler :

Pour (Var i = 10; i >=0; i=i-1) { Essayer { Ecrire (bar(i*3)) } Rattraper (err) { Ecrire (err)} Finalement {Ecrire ("toto")} } Fonction bar (i) { Si (i === 0) Lancer (42); Retourne i; }

Code compilé :

DclVar i # pas obligatoire DclVar bar # pas obligatoire Lambda 39 DclArg i SetVar bar CstRe 3 # debut For SetVar i # i = 10; GetVar i # debut cond. For CstRe 0 GreEqR # i >= 10 ConJmp 30 # Jump vers fin For Continue 23 True # explication 4.0 Continue 27 False GetVar bar StCall GetVar i CstRe 3 MultRe SetArg Call Print Drop Drop # explication 4.2 Jump 10 CstSt "toto" # Finally2, explication 4.1 Print Return SetVar i # Catch Continue 37 False GetVar i Jump 5 CstSt "toto" # Finally3, explication 4.2 Print Throw CstSt "toto" # Finally1, explication 4.1 Print GetVar i CstRe 1 SubsRe SetVar i Jump -34 Jump 8 # debut de la fonction bar GetVar i CstRe 0 Equal ConJmp 2 CstRe 42 Throw GetVar i Return Halt
Explication 4.1: Les Finally

Le Finally est la seule construction qui duplique le code. Le soucis est que si on accède à un Finally via un Return, il faut relancer un Return après. Le plus simple est de dupliquer le code, c'est ce qui est fait dans la pratique (Java va même dupliquer le code après chaque Return...)

Explication 4.1: Le Finally supplémentaire

En fait, il y a un autre cas de finally : le cas où une exception arrive au milieux de Catch (par exemple quand le Catch ne fait qu'ajouter un log). Dans ce cas, il faut quand même exécuter le finally et relancer l'exception après... d'où le troisième finally.

Explication 4.1: Les Drop

Comme tout à l'heure, lorsque je sort du Try sans exception, il me faut supprimer ma continuation d'erreur, mais il me faut aussi supprimer ma continuation de finally. Attention aussi à faire ça avant le finally, car une erreur ou un return dans le finally ne le réexécute pas.