15.07.2013 Views

Les transparents du cours - PPS

Les transparents du cours - PPS

Les transparents du cours - PPS

SHOW MORE
SHOW LESS

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.

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!