Les transparents du cours - PPS
Les transparents du cours - PPS
Les transparents du cours - PPS
Create successful ePaper yourself
Turn your PDF publications into a flip-book with our unique Google optimized e-Paper software.
Compilation Avancée<br />
Cours 3 : Typage<br />
Yann Régis-Gianas<br />
yrg@pps.jussieu.fr<br />
<strong>PPS</strong> - Université Denis Diderot – Paris 7<br />
21 octobre 2009
Références<br />
◮ Types and Programming Languages<br />
Benjamin C. Pierce<br />
◮ Advanced Topics In Types And Programming Languages<br />
sous la direction de Benjamin C. Pierce.
Un type, vous dites ?<br />
◮ Un type spécifie la forme <strong>du</strong> résultat d’un calcul avant que celui-ci n’ait lieu.<br />
◮ Il représente une propriété statique <strong>du</strong> calcul, c’est-à-dire un invariant.<br />
◮ En première approximation, on peut dire qu’une expression est bien typée<br />
si elle promet de faire “bon usage” de l’évaluation de ses sous-expressions.
Quelques expressions plus ou moins bien typées<br />
◮ Si e et f s’évaluent comme des entiers, “e + f” est bien typée.<br />
◮ Si e s’évalue comme un entier, alors “e = "foo"” est mal typée.<br />
◮ Si e s’évalue en un flottant, “sqrt e” est bien typée . . .<br />
(mais si e s’évalue en un flottant négatif, alors le programme explose ! ?)<br />
⇒ Qu’entend-on par “faire bon usage” ?
Sûreté d’un langage de programmation<br />
◮ Soit la relation<br />
e −→ e ′<br />
qui signifie que l’expression e s’évalue en un pas vers l’expression e ′ .<br />
◮ Par exemple, pour les expressions arithmétiques :<br />
◮ Certaines expressions sont bloquées :<br />
1 + 2 + 3 −→ 3 + 3 −→ 6<br />
1 + ”foo” −→<br />
◮ Un interprète ou un compilateur a un comportement spécifié uniquement sur<br />
les termes non bloquants.<br />
◮ Éviter les configurations bloquantes, c’est garantir la sûreté de l’exécution.
Définition formelle d’un système de typage<br />
◮ On écrit “E ⊢ e : T ” avec :<br />
◮ E un environnement de typage (une fonction des identifiants vers les types).<br />
◮ e une expression <strong>du</strong> langage de programmation.<br />
◮ T un type.<br />
pour exprimé le jugement “être bien typé”.<br />
◮ On définit ce jugement à l’aide d’un ensemble fini de règles de la forme :<br />
E ⊢ e : int E ⊢ f : int<br />
E ⊢ e + f : int<br />
◮ Un système de type est dirigé par la syntaxe si il n’existe qu’une et une seule<br />
règle de typage traitant chaque construction syntaxique <strong>du</strong> langage.
À prouver pour chaque langage de programmation !<br />
◮ Si E ⊢ e : T alors l’évaluation de e ne peut pas bloquer.<br />
◮ Se prouve à l’aide de deux propriétés :<br />
◮ Si E ⊢ e : T et e −→ e ′ alors E ⊢ e ′ : T .<br />
◮ Si E ⊢ e : T alors<br />
◮ soit e est le résultat <strong>du</strong> calcul (une valeur)<br />
◮ soit e peut s’évaluer.
À prouver pour chaque langage de programmation !<br />
◮ Si E ⊢ e : T alors l’évaluation de e ne peut pas bloquer.<br />
◮ Se prouve à l’aide de deux propriétés :<br />
◮ Si E ⊢ e : T et e −→ e ′ alors E ⊢ e ′ : T .<br />
◮ Si E ⊢ e : T alors<br />
◮ soit e est le résultat <strong>du</strong> calcul (une valeur)<br />
◮ soit e peut s’évaluer.
Une frontière bien connue<br />
◮ On ne peut pas décider toute propriété statiquement.
La tension<br />
◮ Que faire de la racine carrée ?<br />
Généralité et Richesse des programmes<br />
versus<br />
Précision <strong>du</strong> typage<br />
◮ Ne pas l’inclure dans le langage de programmation, elle est trop dangereuse !<br />
◮ Se donner un type pour les flottants positifs.<br />
◮ Lancer une exception au moment de l’exécution.<br />
◮ “fun x ⇒ x” admet les types :<br />
◮ int → int<br />
◮ bool → bool<br />
◮ Lequel choisir ? Doit-on choisir ?<br />
◮ Si “1 + 1.5” et “1 + 1” sont des expressions bien typées, alors quelles<br />
sémantiques et quels types leur donner ?
Le typage dans un compilateur<br />
Ltypé L1 nontypé L2 nontypé L3 Typage T2 T3<br />
nontypé<br />
◮ Traditionnellement, le typage est une des premières phases des compilateurs.<br />
◮ Dans ce cadre, il sert à vérifier une condition de bonne formation sur laquelle<br />
le compilateur s’appuie aveuglement par la suite.<br />
⇒ Qu’arrive-t-il si une phase de compilation est incorrecte ?
Le typage dans un compilateur<br />
Ltypé L1 typé L2 typé L3 Typage T2 T3<br />
typé<br />
◮ Traditionnellement, le typage est une des premières phases des compilateurs.<br />
◮ Dans ce cadre, il sert à vérifier une condition de bonne formation sur laquelle<br />
le compilateur s’appuie aveuglement par la suite.<br />
⇒ Qu’arrive-t-il si une phase de compilation est incorrecte ?<br />
⇒ Il est plus sûr d’implanter des transformations préservant le bon typage. On<br />
élimine ainsi de nombreuses erreurs.
Un système de type pour Hamlet<br />
e ::= Expression<br />
| x, f , . . . Variable<br />
| 0, 1, . . . , Entier<br />
| e e Application<br />
| fun x ⇒ e Fonction<br />
| fix x. e Point fixe<br />
τ ::= Type<br />
| int Entiers<br />
| τ → τ Fonctions<br />
Γ ::= Environnement de typage<br />
| • Environnement vide<br />
| Γ; (x : τ) Liaison
Un système de types pour Hamlet<br />
(x : τ) ∈ Γ<br />
Γ ⊢ x : τ Γ ⊢ i : int<br />
Γ; (x : τ1) ⊢ e : τ2<br />
Γ ⊢ fun x ⇒ e : τ1 → τ2<br />
i ∈ N<br />
Γ ⊢ e1 : τ1 → τ2 Γ ⊢ e2 : τ1<br />
Γ ⊢ e1 e2 : τ2<br />
Γ; (x : τ1 → τ2) ⊢ e : τ1 → τ2<br />
Γ ⊢ fix x. e : τ1 → τ2
Algorithme de typage<br />
◮ Existe-t-il un algorithme réalisant cette spécification ?
Algorithme de typage<br />
◮ Existe-t-il un algorithme réalisant cette spécification ?<br />
⇒ Et si on modifie la syntaxe de la façon suivante ?<br />
e ::= Expression<br />
| . . .<br />
| fun (x : τ) ⇒ e Fonction<br />
| fix (x : τ). e Point fixe<br />
| . . .
Algorithme de typage<br />
◮ Existe-t-il un algorithme réalisant cette spécification ?<br />
◮ On peut être plus souple en n’exigeant des annotations<br />
seulement lorsque le contexte local n’est pas suffisamment<br />
informatif.<br />
⇒ On obtient alors une forme d’inférence de type partielle.<br />
(x : τ) ∈ Γ<br />
Γ ⊢ x ⇑ τ Γ ⊢ i ⇑ int<br />
Γ ⊢ e1 ⇑ τ1 → τ2 Γ ⊢ e2 ⇓ τ1<br />
Γ ⊢ e1 e2 ⇑ τ2<br />
Γ; (x : τ1 → τ2) ⊢ e ⇓ τ1 → τ2<br />
Γ ⊢ fix x. e ⇓ τ1 → τ2<br />
i ∈ N<br />
Γ; (x : τ1) ⊢ e ⇓ τ2<br />
Γ ⊢ fun x ⇒ e ⇓ τ1 → τ2<br />
Γ ⊢ e ⇑ τ<br />
Γ ⊢ e ⇓ τ<br />
Γ ⊢ e ⇓ τ<br />
Γ ⊢ (e : τ) ⇑ τ
Algorithme de typage<br />
◮ Existe-t-il un algorithme réalisant cette spécification ?<br />
⇒ En absence de toute annotation, un moteur complet<br />
d’inférence des types est nécessaire (c’est le sujet de la seconde<br />
partie de ce <strong>cours</strong>).
Effacement des types<br />
◮ Dans le cadre d’une compilation “classique” (non typée), on efface les<br />
annotations de types une fois que le bon typage <strong>du</strong> programme a été vérifié.<br />
◮ Pour un compilateur préservant les types, on doit tra<strong>du</strong>ire à la fois les<br />
programmes et leurs types, ce qui peut compliquer les tra<strong>du</strong>ction (mais<br />
fournit des garanties sur leurs corrections).
Un système de type pour Clovis<br />
e ::= Expression<br />
| x, f , . . . Variable<br />
| lookup x Extraction dans l’environnement<br />
| 0, 1, . . . , Entier<br />
| let x = e in e Définition locale<br />
| apply e e Application<br />
| closure{x = e|x ⊲ e} Construction de fermeture<br />
| fix x. e Point fixe<br />
v ::= Valeurs<br />
| 0, 1, . . . , Entier<br />
| closure{ρ|x ⊲ e} Fermeture<br />
◮ Le système de type de Hamlet s’applique immédiatement.<br />
⇒ Fournit-il les mêmes garanties en termes de sûreté de l’exécution ?
Exemple problématique<br />
◮ Est-ce que le programme suivant est bien typé ?<br />
◮ Est-ce qu’il s’exécute sans erreur ?<br />
apply (closure{y = 0|x ⊲ y + 1}) 1
Une algèbre de type pour Clovis<br />
τ ::= Type<br />
| int Entiers<br />
| ε (τ) Type construit<br />
ε ∈ {→, local, inenv}<br />
Γ ::= Environnement de typage<br />
| • Environnement vide<br />
| Γ; (x : τ) Liaison<br />
◮ local et inenv sont des constructeurs de type d’arité 1 et “→” est d’arité 2.<br />
◮ On continue à noter l’application de “→” de manière infixe.
Un système de type pour Clovis<br />
(x : local (τ)) ∈ Γ<br />
∀i, Γ ⊢ ei : τi<br />
Γ ⊢ x : τ<br />
(x : inenv (τ)) ∈ Γ<br />
Γ ⊢ lookup x : τ Γ ⊢ i : int<br />
Γ ⊢ e1 : τ1 → τ2 Γ ⊢ e2 : τ1<br />
Γ ⊢ apply e1 e2 : τ2<br />
i ∈ N<br />
Γ; (x1 : local (τ)1); . . .; (xn : inenv (τ)n); (x : local (τ) ′ ) ⊢ e : τ<br />
Γ ⊢ closure{x1 = e1 . . . xn = en|x ⊲ e} : τ ′ → τ<br />
Γ; (x : τ1 → τ2) ⊢ e : τ1 → τ2<br />
Γ ⊢ fix x. e : τ1 → τ2<br />
Γ; x : local (τ1) ⊢ e2 : τ2<br />
Γ ⊢ let x = e1 in e2 : τ2
Un système de type pour Willow<br />
e ::= Expression<br />
| x, . . . Variable<br />
| f Pointeur sur fonctions<br />
| 0, 1, . . . , Entier<br />
| let x = e in e Définition locale<br />
| e (e, e) Appel de fonction<br />
| [e1 . . . en] Création d’un bloc<br />
| e[i] Lecture dans un bloc<br />
| e[i] ← e Écriture dans un bloc<br />
v ::= Valeurs<br />
| 0, 1, . . . , Entier<br />
| f Pointeur sur fonctions<br />
| [v1 . . . vn] Bloc<br />
d ::= f (env, x) = e Définition de fonction
Première tentative<br />
τ ::= Type<br />
| int Entiers<br />
| ε (τ) Type construit<br />
ε ∈ { . →, blocki}<br />
Γ ::= Environnement de typage<br />
| • Environnement vide<br />
| Γ; (x : τ) Liaison<br />
◮ On se donne une famille de constructeurs de type blocki d’arité i.<br />
◮ L’arité de . → est 3. Le type (τenv, τi) . → τo est celui d’une fonction (close) en<br />
attente d’un environnement de type τenv et d’une entrée de type τi pour<br />
pro<strong>du</strong>ire une sortie de type τo.
Deux exemples problématiques<br />
Hamlet let at0 = fun (f : int → int) ⇒ f 0<br />
Clovis let at0 = closure{∅|(f : int → int ⊲ apply f 0}<br />
Willow let at0 = [atcode 0 ; []]<br />
(env, f ) = f [0](f [1], 0)<br />
at code<br />
0<br />
Hamlet let compose = fun (f g : int → int) ⇒ fun (x : int) ⇒ f (g x)<br />
Clovis let compose =<br />
closure{∅|f : int → int ⊲<br />
closure{f = f |g : int → int ⊲<br />
closure{f = f ; g = g|x : int ⊲ apply f (apply g x)}<br />
Willow let compose = [compose code ; []]<br />
compose code (env, f ) = [composef code ; [f ]]<br />
composef code (env, g) = [composefg code ; [g; env[0]]]<br />
composefg code (env, x) = env[1][0](env[1][1], env[0][0](env[0][1], x))<br />
◮ Quels types donner à atcode 0 , composecode , composef code et composefg code ?
Appliquer “quelque soit” le type de l’environnement<br />
Hamlet let at0 = fun (f : int → int) ⇒ f 0<br />
Clovis let at0 = closure{∅|(f : int → int ⊲ apply f 0}<br />
Willow let at0 = [atcode 0 ; []]<br />
(env, f ) = f [0](f [1], 0)<br />
at code<br />
0<br />
◮ Pour que l’expression “f[0] (f[1], 0)” soit bien typée, il faut que le type <strong>du</strong><br />
premier argument atten<strong>du</strong> par “f[0]” soit le même que celui de “f[1]”.<br />
◮ Pour un certain type α, f doit donc avoir le type :<br />
◮ La fonction at code<br />
0<br />
block2 ((α, int) . → int, α)<br />
fonctionne pour tout type α.
Quantification universelle (à la Système F)<br />
τ ::= Type<br />
| int Entiers<br />
| α Variable de types<br />
| ∀α.τ Type polymorphe<br />
| ε (τ) Type construit<br />
ε ∈ { . →, blocki}<br />
e ::= Expression<br />
| . . .<br />
| Λα.e Abstraction de type<br />
| e[τ] Application de type<br />
| . . .<br />
◮ <strong>Les</strong> environnements de typage contiennent des variables de type α.
Nouveaux types, nouvelles règles<br />
Γ; α ⊢ e : τ<br />
Γ ⊢ Λα.e : ∀α.τ<br />
◮ Il est alors possible de réécrire at code<br />
0<br />
at code<br />
0<br />
Γ ⊢ e : ∀α.τ<br />
Γ ⊢ e[τ ′ ] : τ[α ↦→ τ ′ ]<br />
comme ceci :<br />
= Λαβ.fun (env : α, f : block2 ((β, int) . → int; β)) ⇒ f [0](f [1], 0)<br />
◮ La fonction atcode 0 suppose l’existence d’un type β pour l’environnement de f<br />
et se contente de le véhiculer comme une boîte noire.<br />
◮ Comment est éten<strong>du</strong>e la sémantique opérationnelle de Willow ?<br />
◮ Quelles implications a l’ajout <strong>du</strong> polymorphisme sur sa compilation ?<br />
⇒ Réponses dans peu de temps.
Typage de la composition<br />
On pose :<br />
ccode (α) ≡ block2 ((β, int) . → int; β)<br />
Willow let compose = [compose code ; []]<br />
compose code = Λαβ.fun (env : α, f : ccode (β)) ⇒<br />
[composef code ; [f ]]<br />
composef code = Λαβ.fun (env : block1(ccode (α)),<br />
g : ccode (β)) ⇒<br />
[composefg code ; [g; env[0]]]<br />
composefg code = Λαβγ.fun (env : block2(ccode (α); ccode (β)),<br />
x : int) ⇒<br />
env[1][0](env[1][1], env[0][0](env[0][1], x))<br />
◮ Est-ce que ce programme est bien typé ?
Quantification existentielle<br />
◮ Le typage à l’aide de quantification universelle précise trop la forme des<br />
environnements des fermetures.<br />
◮ Par conséquent, les types obtenues dans les fonctions précédentes se<br />
composent mal.<br />
◮ Or, “appliquer une fermeture” est un mécanisme local ne faisant intervenir<br />
que les composantes internes à celle-ci (i.e. son environnement et son code).<br />
◮ Autrement dit, si une fermeture a été construite avec deux composantes<br />
cohérentes, on doit pouvoir par la suite l’appliquer dans tout contexte.<br />
◮ Par contre, on peut oublier la nature exacte de ce type.
Quantification existentielle<br />
τ ::= Type<br />
| int Entiers<br />
| α Variable de types<br />
| ∀α.τ Type polymorphe<br />
| ∃α.τ Type existentiel<br />
| ε (τ) Type construit<br />
ε ∈ { . →, blocki}<br />
e ::= Expression<br />
| . . .<br />
| pack e as ∃α.τ Empaquetage<br />
| unpack e as α, x in e Dépaquetage<br />
| . . .
Nouveaux types, nouvelles règles<br />
Γ ⊢ e : τ[α ↦→ τ ′ ]<br />
Γ ⊢ pack e as ∃α.τ : ∃α.τ<br />
Γ ⊢ e1 : ∃α.τ Γ; α; (x : τ) ⊢ e2 : τ ′<br />
Γ ⊢ unpack e1 as α, x in e2 : τ ′<br />
◮ Il est alors possible de réécrire atcode 0 comme ceci :<br />
atcode 0 = Λα.fun (env : α, f : ∃β.block2 ((β, int) . → int; β)) ⇒<br />
unpack f as γ, code in code[0](code[1], 0)<br />
⇒ Vérifiez le bon typage de cette fonction.<br />
α#τ ′
Solution finale pour le typage des fermetures dans Willow<br />
On pose :<br />
closure ≡ ∃β.block2 ((β, int) . → int; β)<br />
Willow let compose = [compose code ; []]<br />
compose code = Λα.fun (env : α, f : closure) ⇒<br />
pack [composef code ; [f ]] as closure<br />
composef code = fun (env : block1(closure),<br />
g : closure) ⇒<br />
pack [composefg code ; [g; env[0]]] as closure<br />
composefg code = fun (env : block2(closure; closure),<br />
x : int) ⇒<br />
unpack env[1] as α, f<br />
f [0](f [1],<br />
unpack env[0] as β, g in<br />
g[0](g[1], x))
Dualité entre quantification existentielle et universelle<br />
◮ Comme en logique classique, on peut coder une quantification existentielle<br />
par double négation d’une quantification universelle.<br />
pack e as ∃α.τ Λβ.fun (k : ∀α.τ → β) ⇒ k[τe] e<br />
unpack e1 as α, x in e2 e1[τe 2 ] (Λα.fun (x : τ) ⇒ e2)<br />
◮ La variable k représente la continuation <strong>du</strong> calcul.<br />
◮ Un tel codage est dans un style de passage par continuation.<br />
⇒ Nous parlerons des avantages de cette forme de termes pour la compilation.
Flexibilité<br />
Γ ⊢ e1 : τ1 → τ2 Γ ⊢ e2 : τ1<br />
Γ ⊢ e1 e2 : τ2<br />
◮ La règle de l’application interdit la dérivation de typage suivante :<br />
Γ ⊢ f : block2[τ1; τ2] → τ Γ ⊢ [e1; e2; e3] : block3[τ1; τ2; τ3]<br />
qui est pourtant correcte !<br />
Γ ⊢ f [e1; e2; e3] : τ
Sous-typage<br />
◮ L’ensemble des valeurs <strong>du</strong> type “blocki (τ1, . . . , τi)” est inclus dans<br />
l’ensemble des valeurs <strong>du</strong> type “blockj (τ1, . . . , τj)” pour j ≤ i dans le sens<br />
où tout ce qu’on peut faire avec une valeur <strong>du</strong> second type peut être fait<br />
avec une valeur <strong>du</strong> second.<br />
◮ On note cette relation :<br />
◮ La règle suivante est alors correcte :<br />
blocki (τ1, . . . , τi) ⊳ blockj (τ1, . . . , τj)<br />
(Sub)<br />
Γ ⊢ e : τ τ ⊳ τ ′<br />
Γ ⊢ e : τ ′<br />
⇒ C’est un principe de substitutivité des valeurs de type τ ′ par celles de type τ.<br />
⇒ Comment peut-on étendre la relation “⊳” ?
La relation de sous-typage<br />
◮ Il est clair que la relation “⊳” doit être réflexive :<br />
τ ⊳ τ<br />
◮ Le principe de substitutivité est transitif, la relation “⊳” doit l’être aussi :<br />
τ1 ⊳ τ2 τ2 ⊳ τ3<br />
τ1 ⊳ τ3
La relation de sous-typage entre types de bloc<br />
◮ Pour i ≤ j, on a :<br />
blocki (τ1, . . . , τi) ⊳ blockj (τ1, . . . , τj)<br />
◮ Peut-on étendre cette règle en définissant une relation de sous-typage entre<br />
les types de chaque composante ? C’est-à-dire :<br />
i ≤ j ∀k ≤ i, τk ⊳ τ ′ k<br />
blocki (τ1, . . . , τi) ⊳ blockj (τ ′ 1, . . . , τ ′<br />
j )
La relation de sous-typage entre types de bloc<br />
◮ Pour i ≤ j, on a :<br />
blocki (τ1, . . . , τi) ⊳ blockj (τ1, . . . , τj)<br />
◮ Peut-on étendre cette règle en définissant une relation de sous-typage entre<br />
les types de chaque composante ? C’est-à-dire :<br />
i ≤ j ∀k ≤ i, τk ⊳ τ ′ k<br />
blocki (τ1, . . . , τi) ⊳ blockj (τ ′ 1, . . . , τ ′<br />
j )<br />
⇒ Non ! Que dire <strong>du</strong> programme suivant :<br />
let a = [[42; 21]] in<br />
(fun (b : block1(block1(int))) ⇒ b[0] = [17]) a;<br />
a[0][2]<br />
⇒ Cette règle est valide si les champs sont uniquement accessibles en lecture ;<br />
comme c’est le cas, par exemple, pour des n-uplets.
La relation de sous-typage entre types de fonction<br />
◮ Le constructeur de type ”→” est :<br />
τ i ? ⊳ τ i ? τ o ? ⊳ τ o ?<br />
τ i 1 → τ o 1 ⊳ τ i 2 → τ o 2<br />
◮ contravariant sur le type de l’entrée ;<br />
◮ covariant sur le type <strong>du</strong> résultat.
La relation de sous-typage entre types de fonction<br />
◮ Le constructeur de type ”→” est :<br />
τ i 2 ⊳ τ i 1 τ o 1 ⊳ τ o 2<br />
τ i 1 → τ o 1 ⊳ τ i 2 → τ o 2<br />
◮ contravariant sur le type de l’entrée ;<br />
◮ covariant sur le type <strong>du</strong> résultat.
Algorithme de sous-typage<br />
◮ Il n’est pas difficile de réaliser cette relation de sous-typage à l’aide d’une<br />
fonction définie par in<strong>du</strong>ction sur les deux types à comparer.<br />
◮ (Une implémentation efficace est cependant plus subtile à mettre en œuvre.)<br />
◮ Par contre, définir un algorithme de vérification <strong>du</strong> bon typage d’un<br />
programme à partir de notre spécification n’est pas immédiat car cette<br />
dernière n’est pas dirigée par la syntaxe.<br />
◮ Par chance, on peut prouver qu’il existe une spécification équivalente et<br />
dirigée par la syntaxe. Il suffit de supprimer la règle (Sub) et de remplacer la<br />
règle de l’application par :<br />
Γ ⊢ e1 : (τ i 1, τ i 2) . → τ o<br />
Γ ⊢ e2 : τ ′i<br />
1<br />
Γ ⊢ e3 : τ ′i<br />
2<br />
τ ′i<br />
1 ⊳ τ i 1<br />
τ ′i<br />
2 ⊳ τ i 2<br />
Γ ⊢ e1 (e2, e3) : τ o<br />
◮ Il suffit donc de vérifier la relation de sous-typage entre les types atten<strong>du</strong>s<br />
par une fonction et les types de ses arguments réels.
Un système de type pour Toby<br />
e ::= Expression<br />
| x Variable<br />
| 0, 1, . . . , Entier<br />
| e.m(e) Appel de méthode<br />
| new {d} Création d’objet<br />
| δ(e) Application d’une primitive<br />
d ::= Déclaration<br />
| x = e Attribut<br />
| m x = e Méthode<br />
Est-ce suffisant pour définir un système de type pour Toby ?
Un système de type pour Heracles<br />
e ::= Expression<br />
| x Variable<br />
| 0, 1, . . . , Entier<br />
| e.m(e) Appel de méthode<br />
| new A (e) Création d’objet<br />
| δ(e) Application d’une primitive<br />
d ::= Déclaration<br />
| x = e Attribut<br />
| m x = e Méthode<br />
c ::= class A(x) ⊳ A(e){d} Déclaration de classes<br />
Est-ce suffisant pour définir un système de type pour Heracles ?
Différences entre les sous-typage de Toby et d’Heracles<br />
◮ Dans Toby, c’est la structure des valeurs et, uniquement celle-ci, qui<br />
contribue à la relation de sous-typage. On parle de sous-typage structurel.<br />
◮ Dans Heracles, la relation de sous-typage entre deux noms de type est<br />
déclarée par le programmeur en même temps que la relation d’héritage (et<br />
vérifié une fois pour toute). On parle alors de sous-typage nominal.<br />
⇒ Comparez ces deux approches <strong>du</strong> point de vue de :<br />
◮ l’implémentation d’un vérificateur de types ;<br />
◮ l’expressivité <strong>du</strong> langage de programmation ;<br />
◮ l’implémentation d’un interprète ( ?)
Représentation des valeurs d’Heracles<br />
x1 •<br />
B 42 21<br />
A<br />
f y = y + x<br />
x ↦→ 0<br />
x2 •<br />
B 17 73<br />
x3 •<br />
A 21<br />
B<br />
inherits A<br />
y ↦→ 1<br />
◮ La relation de sous-typage nominal in<strong>du</strong>it naturellement une<br />
représentation uniforme des données.<br />
◮ Chaque instance contient une étiquette dé<strong>du</strong>ite <strong>du</strong> nom de sa classe.<br />
◮ Une table associe une description à chaque étiquette.<br />
◮ L’accès à la variable x dans la méthode f se fait toujours à l’indice indiqué<br />
par le descripteur de A, que l’instance considérée soit un A ou un B.
Test d’appartenance<br />
i ∈ T ?<br />
◮ <strong>Les</strong> étiquettes permettent une implémentation efficace de la<br />
primitive instanceof.<br />
⇒ On peut affecter un entier à chaque classe et ré<strong>du</strong>ire ce test à une séquence<br />
de comparaison entre entiers.
Stratégie d’indexation des étiquettes<br />
B [1;3]<br />
D E<br />
◮ L’étiquette est un entier.<br />
A [0;10]<br />
C [4;10]<br />
. . . . . .<br />
◮ instanceof est une comparaison entre l’entier représentant l’étiquette d’une<br />
instance et les deux bornes de la classe considérée.<br />
⇒ Comment rajouter de nouvelles classes dynamiquement dans ce modèle ?<br />
⇒ Nous avons encore fait une hypothèse de monde clos . . .
Stratégie d’indexation incrémentale des étiquettes<br />
B [0;0]<br />
D [0;0;0] E [0;0;1]<br />
A [0]<br />
C [0;1]<br />
. . . . . .<br />
◮ L’étiquette est le chemin menant de la racine à la classe.<br />
◮ instanceof est une comparaison entre ce chemin et le préfixe de l’étiquette<br />
de l’instance considérée.
Représentation des valeurs dans Toby<br />
◮ Pour autoriser un mécanisme de partage de la méthode f , il faut que le code<br />
de celle-ci soit indépendant de l’emplacement des champs.<br />
◮ Une nouvelle indirection semble nécessaire.<br />
x1 • 42 21 x2 • 17 73 x3 • 21<br />
x ↦→ 1<br />
y ↦→ 0<br />
convert<br />
f y = y + x<br />
x ↦→ 0
Compilation par intro<strong>du</strong>ction de coercions<br />
◮ Le sous-typage contraint la représentation des données en forçant une<br />
représentation uniforme ou en intro<strong>du</strong>isant une indirection supplémentaire au<br />
moment de l’exécution.<br />
◮ Une autre stratégie consiste à tra<strong>du</strong>ire un programme Toby faisant usage de<br />
la relation de sous-typage en un programme Toby n’utilisant pas cette règle.<br />
◮ Pour cela, il suffit de tra<strong>du</strong>ire chaque dérivation établissant le sous-typage<br />
entre τ1 et τ2 en une fonction de conversion de type τ1 → τ2,<br />
appelée coercion.
Exemple de coercion : permutation des méthodes<br />
<br />
∀i, Di :: τi ⊳ τ σ(i)<br />
{(m σ(i) : τ σ(i)) i∈[0...n] } ⊳ {(mi : τi) i∈[0...n] }<br />
fun o ⇒ new {<br />
}<br />
(mi x = (Dio.m σ(i))(x)) i∈{0...n}<br />
◮ Cette coercion réordonne les méthodes d’un objet pour les présenter suivant<br />
un arrangement atten<strong>du</strong> par une fonction.<br />
<br />
=
Suppression de deux indirections<br />
x1 • 42 21 x2 • 17 73 x3 • 21<br />
x ↦→ 1<br />
y ↦→ 0<br />
convert<br />
f y = y + x<br />
x ↦→ 0<br />
◮ La fonction convert est appliquée à l’argument avant l’appel de la<br />
méthode f .<br />
◮ L’accès au champs x est un simple décalage de pointeur.<br />
◮ On profite de la connaissance <strong>du</strong> type exact des arguments de f pour la<br />
compiler efficacement.
Représentation des données en présence de polymorphisme<br />
◮ Comment manipuler des données de type inconnu ?<br />
⇒ Comme dans Heracles, une solution possible consiste à représenter<br />
uniformément toutes les valeurs par des pointeurs.<br />
◮ <strong>Les</strong> données doivent donc être allouées (sur le tas) et tout calcul doit<br />
préalablement les déréférencer.<br />
◮ Un entier pourrait être stocké plus efficacement dans un registre . . .<br />
⇒ Comment traiter ces cas particuliers ?<br />
◮ Unboxed objects and polymorphic typing – Xavier Leroy – POPL 92
Le langage Polly<br />
e ::= Expression<br />
| x [τ] Variable<br />
| 0, 1, . . . , Entier<br />
| e e Application<br />
| (e, e) Pair<br />
| fun x ⇒ e Fonction<br />
| letrec x : σ = e in e Définition récursive<br />
τ ::= Type<br />
| α Variable de type<br />
| int Entiers<br />
| τ × τ N-uplets<br />
| τ → τ Fonctions<br />
σ ::= ∀α.τ Schéma de type
Problème <strong>du</strong> polymorphisme<br />
let make_pair x = (x, x + 1) in<br />
let p = make_pair [int] 0 in<br />
fst p + snd p<br />
◮ La fonction make_pair est polymorphe et de type :<br />
∀α.α → α × α
Problème <strong>du</strong> polymorphisme<br />
◮ La fonction make_pair peut pro<strong>du</strong>ire une paire à partir de la représentation<br />
de taille inconnue d’une valeur x de type inconnu α de la façon suivante :<br />
x<br />
make_pair<br />
◮ On connaît toujours la taille d’une adresse.<br />
x<br />
• •<br />
⇒ Deux déréférencements sont nécessaires pour accéder à la valeur d’une<br />
composante.<br />
•
Problème <strong>du</strong> polymorphisme<br />
◮ Si la taille de la représentation de x est connue alors make_pair peut<br />
pro<strong>du</strong>ire une paire plus compacte :<br />
make_pair<br />
x x x
Quelques solutions<br />
◮ On peut spécialiser la fonction make_pair pour chacune de ses instantiations<br />
effectives.<br />
⇒ Duplication <strong>du</strong> code ;<br />
⇒ Temps de compilation important ;<br />
⇒ Hypothèse de monde clos.<br />
◮ À l’exécution, on peut associer un descripteur de type à chaque valeur et<br />
l’observer pour se comporter de façon adéquate.<br />
⇒ Machinerie complexe ;<br />
⇒ Sous-optimal.<br />
◮ On peut :<br />
1. choisir une représentation mixte où les valeurs de type inconnu sont<br />
accessibles à travers des pointeurs tandis que les valeurs de type simple<br />
(entier, flottant, etc) sont des valeurs immédiates ;<br />
2. convertir les valeurs immédiates en pointeur lorsqu’elles sont manipulées dans<br />
un contexte polymorphe et convertir les pointeurs en valeurs immédiates<br />
lorsqu’elles sont pro<strong>du</strong>ites par un contexte polymorphe.<br />
◮ On intro<strong>du</strong>it wrap(τ) qui tra<strong>du</strong>it une valeur de type τ en un pointeur et<br />
unwrap(τ), l’opération inverse.
Règle d’instanciation<br />
◮ La technique d’insertion de coercion est utilisable de nouveau.<br />
◮ Il suffit de suivre la dérivation de typage <strong>du</strong> programme source en tra<strong>du</strong>isant<br />
les instanciations de variables polymorphes par des coercions effectuant les<br />
tra<strong>du</strong>ctions expliquées dans le transparent précédent :<br />
Γ(x) = ∀α.τ ρ ≡ α ↦→ τ<br />
Γ ⊢ x [τ] : ρ(τ) ⇒ Sρ(x : τ)<br />
◮ La tra<strong>du</strong>ction Sρ(e : τ) est définie conjointement à une fonction<br />
<strong>du</strong>ale Gρ(e : τ) :<br />
Sρ(e : α) = unwrap(ρ(α))(e)<br />
Gρ(e : α) = wrap(ρ(α))(e)<br />
Sρ(e : int) = Gρ(e : int) = e<br />
Sρ(e : τ1 × τ2) = Gρ(e : τ1 × τ2) = let x = e in<br />
(Sρ(fst(x) : τ1), Sρ(snd(x) : τ2))<br />
Sρ(e : τ1 → τ2) = fun x.Sρ(e(Gρ(x : τ1)) : τ2)<br />
Gρ(e : τ1 → τ2) = fun x.Gρ(e(Sρ(x : τ1)) : τ2)
Application sur l’exemple<br />
devient<br />
let make_pair x = (x, x + 1) in<br />
let p = make_pair [int] 0 in<br />
fst p + snd p<br />
let make_pair x = (x, x + 1) in<br />
let p = (fun x →<br />
let r = make_pair [int] (wrap (int) x) in<br />
(unwrap (int) (fst r), unwrap (int) (snd r))) 0<br />
in<br />
fst p + snd p
Le langage Apollo<br />
e ::= Expression<br />
| x Variable<br />
| 0, 1, . . . , Entier<br />
| e.m[τ](e) Appel de méthode<br />
| new τ ()e Création d’objet<br />
| δ[τ](e) Application d’une primitive<br />
d ::= Déclaration<br />
| x : τ = e Attribut<br />
| m [α](x : τ) : τ = e Méthode<br />
c ::= class A[α](x) ⊳ A[τ](e){d} Déclaration de classes<br />
τ ::= Type<br />
| α Variable de type<br />
| int Entiers<br />
| ∀α.τ Type polymorphe<br />
| A[τ] Classe instanciée
Compilation par effacement partiel des types<br />
◮ Tout programme écrit en Heracles peut être tra<strong>du</strong>it en un programme écrit<br />
en Apollo.<br />
◮ Un type “Object” vide est intro<strong>du</strong>it et il est sur-type de tout type.<br />
◮ Toute variable de type est remplacée par “Object”.<br />
◮ Toute instanciation de classe paramétrée A[τ] est remplacée par une classe<br />
non paramétrée A.<br />
◮ (C’est ainsi que sont implémentés les Generics de Java.)
Test d’appartenance<br />
◮ L’effacement rend imprécise la fonction instanceof.<br />
◮ Si on a “x : list[int]”, alors le test “instanceof[list[string]](x)” réussit.
Compilation par réification des types<br />
◮ La réification des types consiste à donner une existence concrète aux types<br />
<strong>du</strong>rant l’exécution.<br />
◮ Une méthode polymorphe paramétrée par les types α1, . . . , αn est tra<strong>du</strong>ite<br />
en une méthode attendant n + 1 arguments.<br />
⇒ Une représentation dynamique des types est nécessaire.<br />
⇒ Une implémentation efficace n’est pas simple à mettre en œuvre.<br />
⇒ Nous aborderons ce sujet dans le <strong>cours</strong> sur les machines virtuelles.
Formulation <strong>du</strong> problème de l’inférence de type<br />
◮ L’inférence de type est un procédé visant à reconstruire la dérivation de<br />
typage d’un programme sans (ou avec un minimum) d’annotations de type.<br />
◮ L’inférence de type se ré<strong>du</strong>it à la résolution d’une contrainte entre types.
Ré<strong>du</strong>ction à la résolution de contraintes<br />
◮ On se donne une syntaxe pour les contraintes :<br />
U ::=<br />
| τ = τ<br />
| ∃α.U<br />
| U ∧ U<br />
| ⊥<br />
| ⊤<br />
◮ La contrainte de typage pour qu’une expression e soit de type τ est notée :<br />
Γ ⊢ t : τ
Ré<strong>du</strong>ction à la résolution de contraintes<br />
◮ Γ ⊢ x : τ = (Γ(x) = τ).<br />
◮ Γ ⊢ t1 t2 : τ = ∃α.( Γ ⊢ t1 : α → τ ∧ Γ ⊢ t2 : α )<br />
◮ Γ ⊢ fun x ⇒ t : τ = ∃αα ′ .(α → α ′ = τ) ∧ Γ (x : α) ⊢ t : α ′
Résolution de contraintes<br />
◮ La résolution des contraintes est ici très simple. Il s’agit de traiter chacune<br />
des composantes des conjonctions et d’en conclure la valeur des variables<br />
intro<strong>du</strong>ites existentiellement.<br />
◮ Mais comment résoudre les équations de la forme<br />
?<br />
α ′ → α = int → int
Unification <strong>du</strong> premier ordre<br />
◮ Une substitution est une application qui associe des termes <strong>du</strong> premier ordre<br />
aux variables.<br />
◮ L’application d’une substitution φ à un terme t, notée ˜ φ t est définie ainsi :<br />
˜φ (f t1 . . . tn) = f ( ˜ φ t1) . . . ( ˜ φ tn)<br />
◮ Soit une égalité entre deux termes <strong>du</strong> premier ordre t = t ′ . Le problème<br />
d’unification <strong>du</strong> premier ordre consiste à trouver une substitution de type φ<br />
telle que φ t = φ t ′ .<br />
◮ Une substitution peut être modélisée par une contrainte de la forme :<br />
∃α.α1 = t1 ∧ . . . ∧ αk = tk<br />
◮ On modélise donc l’unification <strong>du</strong> premier ordre comme la réécriture d’une<br />
contrainte .
Multi-équation<br />
◮ Lors de la résolution, on doit "fusionner" les égalités.<br />
◮ Par exemple, α = τ1 ∧ α = τ2 deviendra α = τ1 = τ2.<br />
◮ On se donne une syntaxe pour ses multi-equations qui remplaceront les<br />
équations dans les contraintes :<br />
ɛ ::=<br />
| τ1 = . . . = τn
Multi-équation<br />
Definition (Multi-équation standard)<br />
Une multi-équation est dite standard si toutes les variables qui la composent sont<br />
distinctes et si elle contient au plus un terme qui n’est pas une variable.<br />
◮ α = β = (int → int) est standard.<br />
◮ α = α = (int → int) n’est pas standard.<br />
◮ α = α = (int → int) = int n’est pas standard.<br />
◮ α = (int → int) = (int → β) n’est pas standard.
Implémentation des multi-équations<br />
◮ <strong>Les</strong> multi-équations représentent des classes d’équivalence de variables qui<br />
peuvent être toutes égales à un terme ou bien indéterminées.<br />
◮ L’algorithme d’unification doit fusionner ses classes lorsque deux variables de<br />
deux classes a priori différentes sont mises en correspondance.<br />
◮ L’algorithme d’unification doit trouver le terme associé à une variable<br />
quelconque, c’est-à-dire connaître sa classe d’équivalence, pour pouvoir<br />
égaliser les termes associés à deux variables qu’on met en correspondance.<br />
◮ L’algorithme Union/Find de Tarjan répond exactement à ces deux problèmes<br />
en temps quasi-constant.
Domination<br />
Definition (Domination)<br />
Si U est une conjonction de multi-équations. Une variable α est dominée par β<br />
dans U, noté α U β, si parmi les multi-équations, il en existe une telle que<br />
β = τ avec α qui est une variable libre de τ.<br />
Definition (Cyclicité)<br />
U est cyclique si le graphe de U est cyclique.
Spécification de l’unification<br />
◮ L’unification a pour état une conjonction de multi-équations en forme<br />
standard implémentée par l’ensemble des classes d’équivalence <strong>du</strong><br />
Union/Find. C’est l’ensemble des équations traitées jusqu’à maintenant.<br />
◮ Elle traverse la contrainte d’unification à résoudre et prend en compte<br />
chaque égalité binaire pour en dé<strong>du</strong>ire un nouvel état <strong>du</strong> Union/Find.
Spécification de l’unification<br />
U1 ∧ ∃α.U2 → ∃α.(U1 ∧ U2)<br />
si α /∈ FV(U1)<br />
α = ɛ ∧ α = ɛ ′<br />
→ α = ɛ = ɛ ′<br />
α = α = ɛ → α = ɛ<br />
f t1 . . . ti . . . tn = ɛ → ∃α.(α = ti ∧ f t1 . . . α . . . tn = ɛ)<br />
f α1 . . . αn = f t1 . . . tn = ɛ → α1 = t1 ∧ . . . ∧ αn = tn ∧ f α1 . . . αn = ɛ<br />
f t1 . . . tn = g t ′ 1 . . . t ′ n = ɛ → ⊥<br />
t → ⊤<br />
U ∧ ⊤ → U<br />
U → ⊥<br />
si U est cyclique<br />
U[⊥] → ⊥
Propriétés <strong>du</strong> solveur<br />
◮ → est fortement normalisable.<br />
◮ Si U → U ′ alors U ≡ U ′ .
Propriétés <strong>du</strong> solveur<br />
◮ <strong>Les</strong> formes normales de ce solveur sont :<br />
◮ ⊥, dans ce cas, la contrainte n’est pas satisfiable ;<br />
◮ une conjonction de multi-équations standards.<br />
◮ Une multi-équation en forme standard représente une substitution.
Solveur de contraintes<br />
◮ Le solveur va aussi être spécifié sous la forme d’un système de réécriture.<br />
◮ L’état <strong>du</strong> solveur utilise une pile qui dénote la partie de la syntaxe qu’il reste<br />
à résoudre :<br />
S ::=<br />
| []<br />
| S[[] ∧ C]<br />
| S[∃α.[]]<br />
◮ [] correspond à la position courante <strong>du</strong> solveur dans la contrainte.<br />
◮ L’état <strong>du</strong> solveur est un triplet S; U; C.<br />
◮ S correspond au contexte dans lequel on résout la contrainte U ∧ C.<br />
◮ U est l’ensemble des classes d’équivalence de variables courantes. C’est<br />
l’information connue jusqu’à maintenant.<br />
◮ C est la contrainte qu’on résout.
Solveur de contraintes<br />
S; U; C → S; U ′ ; C<br />
si U → U ′ .<br />
S; ∃α.U; C → S[∃α.[]]; U; C<br />
si α /∈ FV(C).<br />
∃α.S ′ ∧ C; U; C → ∃α.(S ′ ∧ C); U; C<br />
S; U; τ1 = τ2 → S; U ∧ τ1 = τ2; ⊤<br />
S; U; C1 ∧ C2 → S[[] ∧ C2]; U; C1<br />
S; U; ∃α.C → S[∃α.[]]; U; C<br />
S[[] ∧ C]; U; ⊤ → S; U; C
Solveur de contraintes<br />
◮ La spécification <strong>du</strong> slide précédent est une formalisation formelle <strong>du</strong> solveur.<br />
◮ <strong>Les</strong> preuves de correction <strong>du</strong> solveur vis-à-vis de la sémantique des<br />
contraintes sont faites par rapport à cette spécification.<br />
◮ Ces règles nous expliquent simplement :<br />
◮ qu’il faut traiter les composantes des conjonctions une à une.<br />
◮ qu’il faut résoudre les problèmes d’unification.<br />
◮ que les variables existentielles doivent être globalement fraîches.
Exemple<br />
◮ Résoudre la contrainte d’unification :<br />
∃αβ.α → β = int → int
Exemple<br />
◮ Résolution de la contrainte :<br />
∃α. (add : int → int → int) ⊢ λx.add x x : α
Polymorphisme implicite, le langage Olimpia<br />
e ::= Expression<br />
| x Variable<br />
| 0, 1, . . . , Entier<br />
| e e Application<br />
| (e, e) Pair<br />
| fun x ⇒ e Fonction<br />
| let x = e in e Définition locate<br />
τ ::= Type<br />
| α Variable de type<br />
| int Entiers<br />
| τ × τ N-uplets<br />
| τ → τ Fonctions<br />
σ ::= ∀α.τ Schéma de type
Généralisation implicite<br />
◮ Dans une dérivation de typage, une variable de type non contrainte peut être<br />
généralisée :<br />
Gen<br />
Γ ⊢ e : τ α /∈ FV(Γ)<br />
Γ ⊢ e : ∀α.τ<br />
◮ Par exemple, l’identité est polymorphe (elle admet un schéma de type) :<br />
Gen<br />
• ⊢ λx.x : α → α α /∈ FV(•)<br />
• ⊢ λx.x : ∀α.α → α<br />
◮ Ce procédé de généralisation automatique des types est caractéristique des<br />
langages de la famille ML.
Instanciation<br />
◮ Un terme ayant un type polymorphe peut être utilisé dans des contextes de<br />
typage particulier.<br />
Inst<br />
Γ ⊢ t : ∀α.τ<br />
Γ ⊢ x : [α ↦→ τ]τ
Condition pour généraliser une variable<br />
◮ Par contre, on ne peut pas généraliser n’importe quelle variable !<br />
◮ La dérivation suivante est invalide :<br />
Gen<br />
(z : α) ⊢ z : α<br />
(z : α) ⊢ z : ∀α.α<br />
◮ Prouvez que ce type n’est absolument pas sûr !
Restriction<br />
◮ La généralisation et l’instanciation ne sont pas dirigées par la syntaxe. Elles<br />
sont implicites dans le sens où elles peuvent a priori être appliquées à<br />
n’importe quel point de la dérivation de typage.<br />
◮ Dans le cas général, l’inférence de type dans un langage avec types<br />
polymorphes est indécidable . Nous y reviendrons, il s’agit <strong>du</strong> problème de<br />
l’inférence de type pour System F.<br />
◮ Pour rendre ce problème décidable, on restreint le typage de ML à<br />
n’intro<strong>du</strong>ire des schémas de type (utilisation de la règle Gen) qu’au niveau<br />
des let.
Restriction<br />
◮ Dans l’environnement, nous associons maintenant des schémas de type aux<br />
variables.<br />
◮ <strong>Les</strong> règles de typage <strong>du</strong> λ-calcul restent les mêmes : seuls des types<br />
monomorphes sont acceptés sur toutes les constructions exceptés les let :<br />
◮ et les variables :<br />
Let<br />
Γ ⊢ t1 : σ Γ (x : σ) ⊢ t2 : τ ′<br />
Γ ⊢ let x = t1 in t2 : τ ′<br />
Var<br />
(x : σ) ∈ Γ<br />
Γ ⊢ x : σ
Restriction<br />
◮ Comme les schémas de type ne sont acceptés que sur les lets, une dérivation<br />
de typage n’effectue une généralisation qu’après une règle Let. <strong>Les</strong> autres<br />
éventuelles généralisations sont nécessairement suivies d’une instanciation<br />
pour retrouver un type monomorphe compatible avec les règles <strong>du</strong> λ-calcul .<br />
◮ On peut donc supprimer ces successions de règles Gen/Inst et factoriser la<br />
règle Let et la règle Gen :<br />
LetGen<br />
Γ ⊢ t1 : τ Γ (x : σ) ⊢ t2 : τ ′<br />
α /∈ FV(Γ)<br />
Γ ⊢ let x = t1 in t2 : τ ′<br />
◮ De même, on peut aussi factoriser la règle Var et la règle Inst :<br />
VarInst<br />
(x : σ) ∈ Γ σ τ<br />
Γ ⊢ x : τ
Inférence de types pour Olimpia<br />
◮ Dès qu’on rajoute la construction let , les choses se compliquent : on doit<br />
inférer les applications de la règle Gen et de la règle Inst.<br />
◮ Heureusement, on sait que la généralisation peut se faire uniquement au<br />
niveau des let et l’instanciation uniquement au niveau des variables.<br />
◮ Quelles vont être les contraintes capturant exactement le typage de ses<br />
constructions ?
Contraintes liées à l’inférence de type<br />
◮ On se donne deux nouvelles constructions de contraintes :<br />
C ::=<br />
| . . .<br />
| let (x : ∀α[C]τ) in C<br />
| x τ<br />
◮ La construction let intro<strong>du</strong>it des variables qui pourront être généralisées .
Contraintes associées<br />
◮ Grâce au let, il n’est plus nécessaire de maintenir un environnement dans le<br />
jugement d’inférence.<br />
◮ let x = t1 in t2 : τ =<br />
let (x : ∀α[ Γ ⊢ t1 : α ]α) in Γ ⊢ t2 : τ <br />
◮ Le schéma associé à x est un schéma contraint .<br />
◮ x : τ = x τ<br />
◮ x τ signifie<br />
∃α.(U ∧ (α = τ))<br />
si ∀α[U]α est le schéma associé à x dans le contexte courant.
Exemple<br />
◮ Quelles sont les contraintes liées aux programmes suivants ?<br />
1. let iszero = λx.(x = 0) in iszero<br />
2. let id = λx.x in id 0<br />
3. let app = λf .λx.f x in app id 0
Résolution des contraintes<br />
S[let (x : ∀α[∃α ′ .[]]τ) in C ′ ]; U; C → S[let (x : ∀αα ′ [[]]τ) in C ′ ]; U; C<br />
si α ′ /∈ FV(τ).<br />
S[let (x : ∀α[C ′ ]τ) in ∃α.[]]; U; C → S[∃α ′ .let (x : ∀α[C ′ ]τ) in []]; U; C<br />
si α ′ /∈ FV(τC ′ ).<br />
S; U; let (x : ∀α[C ′ ]τ) in C → S[let (x : ∀α[[]]τ) in C]; U; C ′<br />
si α ′ /∈ FV(U).<br />
S[let (x : ∀α[[]]τ) in C]; U; ⊤ → S[let (x : ∀αα[[]]α) in C]; U ∧ α = τ; ⊤<br />
S[let (x : ∀αα[[]]α ′ ) in C]; α = β = ɛ ∧ U; ⊤ →<br />
S[let (x : ∀αα[[]]θ (α ′ )) in C]; β = θ (ɛ) ∧ θ (U); ⊤<br />
si α = β et θ ≡ [α ↦→ β].<br />
si α /∈ FV(Uτ) et τ n’est pas une variable.<br />
-
Résolution des contraintes<br />
S[let (x : ∀αα[[]]α ′ ) in C]; α = ɛ ∧ U; ⊤ → S[let (x : ∀α[[]]α ′ ) in C]; ɛ ∧ U; ⊤<br />
si α ′ /∈ α ∪ FV(ɛU).<br />
S[let (x : ∀αβ[[]]α ′ ) in C]; U; ⊤ → S[∃β.let (x : ∀α[[]]α ′ ) in C]; U; ⊤<br />
si β /∈ FV(C)<br />
et ∃α.U détermine les β.<br />
S[let (x : ∀α[[]]α ′ ) in C]; U1 ∧ U2; ⊤ → S[let (x : ∀α[U2]α ′ ) in []]; U1; C<br />
S[let (x : ∀α[U ′ ]α ′ ) in []]; U; ⊤ → S; U; ⊤<br />
si α /∈ FV(U1)<br />
et ∃α.U2 ≡ ⊤.<br />
S; U; x τ → S; U; ∃α.(U ′ ∧ (α = τ))<br />
si S(x) = ∀α[U ′ ]α.
Résolution des contraintes<br />
Definition (Détermination)<br />
On dit que C détermine α si quelque soit la valeur des variables libres de C qui<br />
ne sont pas dans α, il n’y a qu’une unique valeur possible pour les α.<br />
◮ La contrainte α = β1 → β2 détermine β1β2 et détermine α.<br />
◮ La contrainte ∃β1.α = β1 → β2 ne détermine pas α.
Résolution des contraintes<br />
Dur, <strong>du</strong>r ... ?
Résolution des contraintes<br />
◮ Pas d’inquiétude, nous allons expliquer ces règles.<br />
◮ Avant cela, nous allons concrétiser un peu plus l’environnement <strong>du</strong> solveur.
Résolution des contraintes<br />
◮ Si on se focalise sur la pile <strong>du</strong> solveur (le contexte de résolution), on<br />
remarque qu’on peut écrire le solveur sous la forme d’une fonction récursive<br />
qui :<br />
◮ Parcourt la contrainte en profondeur ;<br />
◮ Résout des problèmes d’unification aux feuilles ;<br />
◮ En remontant sur les lets, se pose la question de la généralisation des<br />
variables pour en dé<strong>du</strong>ire le schéma de type contraint à inférer.<br />
◮ Le squelette <strong>du</strong> solveur est donc :<br />
let rec solve env = function<br />
| CAnd (c1, c2) →<br />
let env = solve env c1 in<br />
solve env c2<br />
| CLet (x, Scheme(vs, c1, v), c2) →<br />
let env = intro<strong>du</strong>ce true env vs in<br />
let env = solve env c1 in<br />
let gen_vs, old_vs = generalize env in<br />
let env = solve (bind env x v) c2 in<br />
pop_let env old_vs<br />
(* ... *)
Résolution des contraintes<br />
◮ La fonction essentielle est la généralisation .<br />
◮ Pour l’implémenter efficacement, il faut savoir en temps constant pour<br />
chaque variable à quelle profondeur de let elle a été intro<strong>du</strong>ite .<br />
◮ On associe à chaque variable un rang dénotant cette profondeur.<br />
◮ Que doit-on faire <strong>du</strong> rang lorsqu’on unifie deux variables ?<br />
◮ Plus généralement, que faire lorsqu’on intro<strong>du</strong>it une variable de rang k dans<br />
une multi-équation dont toutes les variables ont un rang k ′ ?<br />
◮ On dira qu’une variable est vieille lorsqu’elle est intro<strong>du</strong>ite par un let<br />
englobant le plus profond let en <strong>cours</strong> de résolution.<br />
◮ On dira qu’une variable est jeune lorsqu’elle est intro<strong>du</strong>ite par le plus profond<br />
let en <strong>cours</strong> de résolution.
Résolution des contraintes<br />
S[let (x : ∀α[∃α ′ .[]]τ) in C ′ ]; U; C → S[let (x : ∀αα ′ [[]]τ) in C ′ ]; U; C<br />
si α ′ /∈ FV(τ).<br />
◮ <strong>Les</strong> variables existentielles intro<strong>du</strong>ites dans la contrainte gauche d’un let sont<br />
généralisables par le let le plus proche. Quel est le rang de ces variables ?
Résolution des contraintes<br />
S[let (x : ∀α[C ′ ]τ) in ∃α.[]]; U; C → S[∃α ′ .let (x : ∀α[C ′ ]τ) in []]; U; C<br />
si α ′ /∈ FV(τC ′ ).<br />
◮ <strong>Les</strong> variables qui interviennent dans la contrainte droite d’un let peuvent<br />
remonter si elles sont suffisamment fraîches. Quel est le rang de ces<br />
variables ?
Résolution des contraintes<br />
S; U; let (x : ∀α[C ′ ]τ) in C → S[let (x : ∀α[[]]τ) in C]; U; C ′<br />
si α /∈ FV(U).<br />
◮ <strong>Les</strong> variables intro<strong>du</strong>ites par un let doivent être fraîches. Quel est le rang de<br />
ces variables ?
Résolution des contraintes<br />
S[let (x : ∀α[[]]τ) in C]; U; ⊤ → S[let (x : ∀αα[[]]α) in C]; U ∧ α = τ; ⊤<br />
si α /∈ FV(Uτ) et τ n’est pas une variable.<br />
◮ <strong>Les</strong> types intervenants dans les lets peuvent être des variables qui jouent le<br />
rôle de pointeurs. Ainsi, on augmente le partage et on minimise la recopie.<br />
◮ Ce partage est nécessaire pour obtenir une complexité linéaire en moyenne<br />
de l’algorithme.<br />
◮ En particulier, on doit maintenir l’invariant que tout type intro<strong>du</strong>it dans un<br />
let de rang k est associé à une multi-équation de rang k.
Résolution des contraintes<br />
S[let (x : ∀αα[[]]α ′ ) in C]; α = β = ɛ ∧ U; ⊤ → -<br />
S[let (x : ∀αα[[]]θ (α ′ )) in C]; β = θ (ɛ) ∧ θ (U); ⊤<br />
si α = β et θ ≡ [α ↦→ β].<br />
◮ Lorsque deux variables sont mises en correspondance par une équation, on<br />
peut substituer l’une par l’autre dans toute la contrainte. Cette règle justifie<br />
l’utilisation de l’algorithme Union/Find. <strong>Les</strong> variables sont moralement<br />
dénotées par le représentant de leur classe d’équivalence.
Résolution des contraintes<br />
S[let (x : ∀αα[[]]α ′ ) in C]; α = ɛ ∧ U; ⊤ → S[let (x : ∀α[[]]α ′ ) in C]; ɛ ∧ U; ⊤<br />
si α ′ /∈ α ∪ FV(ɛU).<br />
◮ On peut "jeter" les variables qui n’ont plus de rôle dans la contrainte.
Résolution des contraintes<br />
S[let (x : ∀αβ[[]]α ′ ) in C]; U; ⊤ → S[∃β.let (x : ∀α[[]]α ′ ) in C]; U; ⊤<br />
si β /∈ FV(C)<br />
et ∃α.U détermine les β.<br />
◮ On peut partionner l’ensemble des variables généralisables au niveau d’un let<br />
en deux parties : l’une représentant les variables qui sont les paramètres<br />
véritables de la contrainte let et les variables qui sont indépendantes de cette<br />
contrainte.
Résolution des contraintes<br />
Theorem (Décision de la détermination)<br />
Soient α et β deux ensembles de variables distinctes.<br />
◮ Si ɛ est γ = ɛ ′ avec γ /∈ αβ et β ⊆ FV(ɛ ′ ),<br />
◮ ou bien ɛ est β = τ = ɛ ′ avec FV(τ) sans intersection avec les α et β.<br />
◮ Alors ∃α.(C ∧ ɛ) détermine les β.<br />
◮ Cas 1 : α = β1 → β2 avec α vieille alors β1 et β2 sont déterminées par cette<br />
équation.<br />
◮ Cas 2 : α = β1 → β2 avec β1 et β2 vieilles alors α est déterminées par cette<br />
équation.
Résolution des contraintes<br />
◮ On peut en dé<strong>du</strong>ire un algorithme efficace pour résoudre le problème<br />
suivant : soit ∃α.U, trouver β telles que ∃(α \ β).U détermine les β.<br />
◮ Réfléchissez un peu sur l’exemple :<br />
let (x : ∀α1α2α3[let (y : ∀β1β2β3[β1 = α2 → α3 ∧ α1 = β2 → β3]β1) in ⊤]α1) in ⊤
Résolution des contraintes<br />
◮ L’algorithme cherche à déterminer parmi les α, celles sont qui sont devenues<br />
vieilles après unification.<br />
◮ On parcourt donc U pour mettre à jour les rangs des variables.<br />
◮ <strong>Les</strong> variables dont le rang sera resté égal au rang <strong>du</strong> let courant pourront<br />
être généralisées : elles ne sont pas déterminées par des variables plus vieilles.<br />
◮ La mise à jour des rangs utilisent ces deux propriétés (venant <strong>du</strong> théorème<br />
précédent) :<br />
1. Etre dominé par une variable plus vieille rend vieux.<br />
2. Si toutes les variables que dominent une variable sont vieilles alors cette<br />
variable est vieille.
Résolution des contraintes<br />
◮ L’algorithme commence donc par propager la vieillesse des variables les plus<br />
vieilles vers leurs successeurs par la relation de domination.<br />
◮ <strong>Les</strong> successeurs propagent ensuite leur vieillesse vers les variables dominantes.
Résolution des contraintes<br />
S[let (x : ∀α[[]]α ′ ) in C]; U1 ∧ U2; ⊤ → S[let (x : ∀α[U2]α ′ ) in []]; U1; C<br />
si α /∈ FV(U1)<br />
et ∃α.U2 ≡ ⊤.<br />
◮ Le traitement <strong>du</strong> membre gauche d’une contrainte let est terminé si<br />
l’ensemble des informations apprises sur les variables liées sur cet let est<br />
satisfiable.
Résolution des contraintes<br />
S[let (x : ∀α[U ′ ]α ′ ) in []]; U; ⊤ → S; U; ⊤<br />
◮ Une fois qu’on a montré que la partie droite d’une contrainte let est<br />
satisfiable, on peut dépiler le let.
Résolution des contraintes<br />
S; U; x τ → S; U; ∃α.(U ′ ∧ (α = τ))<br />
si S(x) = ∀α[U ′ ]α.<br />
◮ L’instanciation de x, c’est la <strong>du</strong>plication <strong>du</strong> schéma associé à x dans la<br />
contrainte courante. Il faut bien sûr choisir les α frais.