Planche : 1


Compilation


Planche : 2


A
Qu’est-ce que compiler ?
B
Machine abstraite.
C
Compilation de PCF.

Planche : 3


Presque un ordinateur

Programmer l’ENIAC (1946), c’est connecter des câbles.


Planche : 4


Un peu plus tard (Iris 10 010, 1967)

Programmer un ordinateur, c’est entrer un programme en mémoire.


Planche : 5


Un ordinateur

Par définition ou presque (machine de Von Neumann), un ordinateur est :


Planche : 6


Entrer un programme en mémoire

Il s’agit d’un détail technique. Par exemple…

Années 80, « amateur » fauché    Aujourd’hui, assembleur
    

Planche : 7


Et PCF ?

Il n’existe pas d’ordinateur qui exécutent les programmes PCF directement.

Car PCF s’addresse d’abord aux être humains (sisi).

Deux techniques pour exécuter PCF.


Planche : 8


Machine abstraite

Traduire PCF vers une vraie machine, c’est trop compliqué.

Car la vraie machine est un peu trop simple, et il y aura des complications pour les fonctions.

À la place, nous traduisons vers une machine abstraite. Puis…


Planche : 9


Poule et œuf

Imaginons que PCF est le seul langage L (informatique) de l’humanité, qui vient d’inventer son premier ordinateur M. Pour pouvoir exécuter un programme écrit en L, il faut :

Ensuite…

C’est en gros ce qui s’est passé (mais avec Fortran.)

L’écriture d’un compilateur dans son propre langage est le bootstrap (auto-amorçage).


Planche : 10


Poule et œuf du troisième millénaire

Soit un langage informatique L déjà compilable (et bootstrapé) sur une machine M.

Je note LL M le source et LM M l’exécutable (du compilateur).

Une nouvelle machine M′ arrive sur le marché.

Si LL M est buggé, il est parfois difficile de corriger.


Planche : 11


Premiers pas

Une instruction :

L’instruction (dite load ou move immédiat) range une constante entière dans un registre.

Un registre est une case de mémoire disponible à l’intérieur de l’unité de contrôle.


Planche : 12


Opérations arithmétiques

Inteprétation de t1 + t2

let rec interp … t = match t with
Op (Add,t1,t2) ->
    let n1 = match interp … t1 with Int n -> n in
    let n2 = match interp … t2 with Int n -> n in
    Int (n1+n2)

Autrement dit,

Comment faire, avec la machine qui ne connaît, ni la liaison let, ni l’appel de fonction (et encore moins l’appel récursif).


Planche : 13


Calculer avec une pile

Le code est une séquence d’instructions.

Remplacer « Interpréter t» par, executer le code qui calcule la valeur de t (dans l’accu), noté C(t).



Soit compilation de t1 + t2.

C(t1) ; Push ; C(t2) ; Add

NB : C est la fonction de compilation, des termes PCF vers le code.


Planche : 14


Machine abstraite à pile

État de la machine : un triplet accu × pile × code, noté (A,S,C)

Effet des instructions : sémantique à petit pas (système de transitions).

(_, S, (Ldi nC))(nSC)
(AS, (PushC))(A, (AS), C)
(n2, (n1;S), (AddC))(n1+n2SC)

Le résultat du calcul est un état de la forme (n,∅,∅).


Planche : 15


Examples

Simple :

C(1+2) = Ldi 1; PushLdi 2; Add

Simple :

C(3+4) = Ldi 3; PushLdi 4; Add

Plus compliqué :

C((1+2)+(3+4))= C(1+2); PushC(3+4) ; Add
  =  



Ldi 1; PushLdi 2; Add;
Push;
Ldi 3; PushLdi 4; Add;
Add

À la fin, l’accumulateur contient 10 et la pile  est comme au début.


Planche : 16


Machine abstraite en Caml


Planche : 17


La pile en vrai

Il est assez facile de réaliser une pile en machine.

Si la machine donne directement les instructions push et pop.


Planche : 18


Sans instructions push/pop

L’addresse du sommet de la pile est dans un registre particulier dit pointeur de pile (%esp).

Push
    subl $4,%esp
    movl %eax,0(%esp)
Pop
    movl 0(%esp),%eax
    addl $4,%eax
  

Planche : 19


Push sur la vraie machine

État de la vraie machine :


Planche : 20


Addition en pile sur la vraie machine


Planche : 21


Compilation

Comme une définition de la fonction C.

C(n)=Ldi n
C(t1 op t2)=C(t1) ; Push ; C(t2) ; Cop op

En Caml.

let rec compile t = match t with
Num n -> [Ldi n]
Op (op,t1,t2) -> compile t1 @ [Push@ compile t2 @ [Cop op]
| …

Ou encore (éviter les concaténations).

let rec compile_k t k = match t with
Num n -> Ldi i::k
Op (op,t1,t2) -> compile_k t1 (Push::compile_k t2 (Cop op::k))

(compile t @ c = compile_k t c, pour tout terme t et tout code c)


Planche : 22


La conditionelle

Interprétation.

E ⊢ t10           E ⊢ t2v
E ⊢ Ifz t1 Then t2 Else t3v
E ⊢ t1n (n ≠ 0)           E ⊢ t2v
E ⊢ Ifz t1 Then t2 Else t3v

La compilation semble évidente :


Planche : 23


Tout simplement

On se donne une instruction Test(C2,C3) qui va exécuter C2 ou C3 selon la valeur de l’accumulateur. Genre

(0,S,Test(C2,C3))(0,S,C2)
(n,S,Test(C2,C3))(n,S,C3)  (n ≠0)

Et on compile :

C(Ifz t1 Then t2 Else t3) = C(t1); Test(C(t2),C(t3))

Planche : 24


Pas si simple

Considérer :

(Ifz t1 Then t2 Else t3)+3

On aura un code du genre

C(t1); Test(C(t2),C(t3)); PushLdi 3; Add

C’est à dire que les transitions de la machine abstraite sont plutôt :

(0,S,(Test(C2,C3); C)(0,S,(C2C))
(n,S,(Test(C2,C3); C))(n,S,(C3C))  (n ≠0)

Dans une implémentation Caml, on écrira malheureusement:

let rec run a s c = match a,s,c with

Int 0,_,(Test (c2,c3)::c) -> run a s (c2@c)

Mais on peut éviter la concaténation avec un peu de reflexion.


Planche : 25


Conditionelle, vraie machine

Dans la vraie machine, pas de liste d’instruction en argument des instructions !

Une seule séquence, et des instructions de saut.

    C(t1)
    testl %eax,%eax
    je L2         #Test (L2,L3)
L3:
    C(t3)
    jmp Lk
L2
:
    C(t2)
Lk:
    pushl %eax     #Push
    movl $3,%eax   #Ldi 3
    addl 0(%esp)
    addl $4,%esp   #Add

Planche : 26


Les environnements

Les règle de la variable et du Let.

E(x) = v
E ⊢ xv
E ⊢ t1v1           E ⊕ [x = v1] ⊢ t2v
E ⊢ Let x = t1 In t2v

(Tiens nous sommes en appel par valeur).

On ajoute donc:

Si Search appelle List.assoc, on a pas compilé grand chose.


Planche : 27


Retour sur les accès aux variables

Considérons :

Let x = t1 In
Let
 y = t2 In
Let
 z = t3 In
t

Au moment d’interpréter t, l’environnement est de de la forme :


Planche : 28


Compilation des accès aux variables

Au vu du programme la position des variables dans l’environnement à l’exécution (E) est connue (pour z, c’est 0,…, pour x c’est 2).

On peut donc simplifier E: c’est une liste de valeurs.

(A,S,E,(ExtendC))(A,S,(A;E),C)
(_,S,(v0v1; ⋯; vk; ⋯),(Search kC))(vk,S,(v0; ⋯; vk; ⋯),C)

Compilation, CE(t) où E donne les positions des variables (liste de variables suffit ici).

CE(Let x = t1 In t2)=CE(t1) ; Extend ; Cx;E(t2)
C[x0;⋯;xk ;⋯](xk)=Search k

Planche : 29


Mais attention

Let x = 2 In
(Let x = 1 in x+x)+x

Il faut après le code C[x](Let x = 1 In x+x) redonner sa valeur d’origine à l’environnement.

Une solution simple est de sauver/restaurer l’environnement à l’aide de la pile S.

(A,S,E,(PushEnvC))(A,(E;S),E,C)
(A,(E;S),_,(PopEnvC))(A,S,E,C)

Et compilation complète du Let.

CE(Let x = t1 In t2)=PushEnvCE(t1); ExtendCx;E(t2); PopEnv

Planche : 30


La vraie machine

Différence essentielle entre pile et environnement :

Il fait donc ajouter du code supplémentaire au code compilé, qui gère l’allocation mémoire (facile) et la récupération (plus dur). Ce code supplémentaire s’appelle parfois un environnement d’exécution (argl) ou encore le support à l’exécution.

Mais le principe de changer les noms de variables en positions est typique.


Planche : 31


Machine abstraite en Caml

On utilise la gestion mémoire de Caml.

let rec run a s e c = match a,s,c with

_,_,(Extend::c) -> run a s (a::ec

On utilise donc le support à l’exécution de Caml, sans se poser plus de questions.

Mais si on écrit une machine abstraite en assembleur (en C), il faudra se poser des questions (réponse malloc + écrire un GC).


Planche : 32


Fonctions

Interprétation de la fonction.

E ⊢ Fun x -> t↪<xtE>

La fermeture en machine est une paire <CE> (t compilé, et plus besoin du nom de la variable).

Par exemple :

Let y = ty In
Let
 f = Fun x -> x+y In

La fermeture suivante représente f.

<Search 0; PushSearch 1; Addvy>

Planche : 33


Compilation

Une nouvelle instruction pour fabriquer les fermetures :

(_,S,E,(MakeClo [C]; C′))(<CE>,S,E,C′)

On ose à peine préciser :

let rec run a s e c  = match a,s,c with

_,_,(MakeClo c::c') -> run (Clo (c,e)) s e c'

Compilation, seuls les environnements sont (un peu) subtils.

CE(Fun x -> t)=MakeClo [Cx;E(t)]

Noter quand même qu’une fermeture est la valeur d’une fonction, et se retrouve donc logiquement dans l’accumulateur.


Planche : 34


Application

La règle d’inteprétation.

E ⊢ t1↪<xtE′>           E ⊢ t2v2           E′ ⊕ [x=v2] ⊢ tv
E ⊢ t1 t2v

Soit il faut :

Donc une instruction Apply fait tout ça

(<C′•E′>,(vS), EApply)(A,∅,(v;E′),C′)

Et compilation :

CE(t1 t2)=CE(t2); PushCE(t1) ; Apply

Mais une fois de plus c’est un peu trop simple…


Planche : 35


Application en contexte

Let x = 1 In
Let
 f = Fun x -> x+x In
(f 2) + x

On a donc, par compilation, un code du genre:

Ldi 2; Push; Search 0; Apply; CE(x); Add

(ou E est [f; x; ⋯])

Il faut donc retrouver l’état de la machine après le retour de la fonction.


Planche : 36


Sauver l’état de la machine, sur la pile

Un état de machine est normalement (A,S,E,C), mais


Planche : 37


Compilation de l’appel de fonction

Avec Apply qui sauve l’état de la machine, c’est simple.

CE(t1 t2)=CE(t2); Push; CE(t1); Apply

Compte tenu de ce qui vient d’être vu, à la fin de la séquence ci dessus.


Planche : 38


Modèle du poly

Sauvegarde et restauration explicite de l’environnement par l’appelant.

CE(t1 t2) = PushEnv; CE(t2); Push; CE(t1); Apply; PopEnv

Avec un Apply moins puissant :

(A,(E;S),_,(PopEnv;C)(A,S,E,C)
(<C′•E′>,(vS), E, (ApplyC))(A,S,(v;E′),(C′;C))

N.B. La compilation du poly, produit toujours Apply suivi de PopEnv.

Le modèle du poly n’a pas de règle spéciale pour le retour de fonction (mais il triche un peu dans la règle de Apply).


Planche : 39


Un miracle

Soit une fonction à deux arguments:

Let k = Fun x -> Fun y -> x+y In
k
 1 2

Soit Let k = tk In t. Le code de tk est :

MakeClo [MakeClo [Search 1; Push; Search 0; Add]]

Son exécution construit la fermeture c = <MakeClo C•∅>, avec C = [Search 1; ⋯].

Et le code de t est :

Ldi 2; Push;
   Ldi 1; Push; Search 0; Apply;
Apply;

Planche : 40


Appel de fonction à deux arguments

Au moment du premier Apply.

(<MakeClo [C]•∅>, (1;2), E, (Apply; Apply; ∅))

(Où E est l’environnement donne sa valeur à k.)

Transition Apply

(_, ((Apply; ∅); E; 2), (1;E), MakeClo C)

Exécution de MakeClo C.

(<C•(1;E)>, ((Apply; ∅); E; 2), (1;E), ∅)

Retour de fonction.

(<C•(1;E)>, (2), E, (Apply; ∅))

Nouvel Apply :

(_, ((∅);E), (2;1;E), C)

C’est à dire que le code C=[Search 1; Push; Search 0; Add] s’exécute dans l’environnement approprié (x=1 en seconde position, y=2 en première).

À la fin du code C.

(3, ((∅);E), (2;1;E), ∅)

Et retour de fonction.

(3, ∅, E, ∅)

Planche : 41


Le point fixe

Nous ne considérons que le point fixe de fonctions Fix f -> Fun x -> t.

Interprétation par une fermeture bouclée.

On invente une nouvelle instruction MakeCloRec qui construit ce style de fermeture bouclée, mais pour la machine.

Avec donc la règle d’exécution :

(_,S,E,(MakeCloRec [C];C′)) → (c,S,E,C′),   où c graphe c = <Cc;E>

À comparer avec :

(_,S,E,(MakeClo [C]; C′)) → (<CE>,S,E,C′)

Planche : 42


On ose à peine préciser

Exécution en Caml.

let rec run a s e c  = match a,s,c with

_,_,(MakeCloRec c::c') ->
  let rec clo_rec = Clo (c,clo_rec::ein
  run clo_rec s e c'

Compilation, seuls les environnements sont (un peu) subtils.

CE(Fix f -> Fun x -> t)=MakeCloRec [Cx;f;E(t)]

N.B. La transition Apply ne change pas.

Évidemment rien n’empèche de se passer de MakeClo.

CE(Fun x -> t)=MakeCloRec [Cx; _; E(t)]

Planche : 43



Planche : 44


Les transitions de la machine (A,S,E,C)

(_, SE, (Ldi nC))(nSEC)
(ASE, (PushC))(A, (AS), EC)
(n2, (n1;S), E, (AddC))(n1+n2SEC)
(0,S,E,(Test(C2,C3);C))(0,S,E,(C2C))
(n,S,E,(Test(C2,C3);C))(n,S,E,(C3C))  (n ≠0)
(A,S,E,(ExtendC))(A,S,(A;E),C)
(_,S,(v0v1; ⋯; vk; ⋯),(Search kC))(vk,S,(v0; ⋯; vk; ⋯),C)
(A,S,E,(PushEnvC))(A,(E;S),E,C)
(A,(E;S),_,(PopEnvC))(A,S,E,C)

Transitions pour les fonctions

(_,S,E,(MakeClo [C]; C′))(<CE>,S,E,C′)
(_,S,E,(MakeCloRec [C];C′))(c,S,E,C′),   avec c = <Cc;E>
(<C′•E′>,(vS), E, (ApplyC))(A,(CE ; S),(v;E′),C′)
(A,(C;E;S),_,∅)(A,S,E,C)

État final de la machine :

(v, ∅, _, ∅)

(Pile et code vides). Le résultat est v, le contenu de l’accumulateur.


Planche : 45


Compilation de PCF

CE(n)=Ldi n
CE(t1 + t2)=CE(t1); PushCE(t2); Add
CE(Ifz t1 Then t2 Else t3)=CE(t1); Test(CE(t2),CE(t3))
CE(Let x = t1 In t2)=PushEnv; CE(t1); ExtendCx;E(t2); PopEnv
C[x0;⋯;xk;⋯](xk)=Search k
CE(t1 t2)=CE(t2); Push; CE(t1); Apply
CE(Fun x -> t)=MakeClo [Cx;E(t)]
CE(Fix f -> Fun x -> t)=MakeCloRec [Cx;f;E(t)]

Sauf erreur, bien entendu.


Ce document a été traduit de LATEX par HEVEA