Compilateur orienté lambda calcul

128
λ

description

mémoire de fin d'études universitaires en ingénierie informatique réalisé par : MASTOUR mohamed et FEGHOUL Hakim

Transcript of Compilateur orienté lambda calcul

Page 1: Compilateur orienté lambda calcul

Compilateur orienté λ calcul

mémoire présenté par :FEGHOUL hakim

MASTOUR Mohamedpour l'obtention du diplôme d'ingénieur d'état en informatique

U.S.T.O

année universitaire 2000/2001

Page 2: Compilateur orienté lambda calcul

Remerciement

�Nous exprimons notre profonde gratitude à nos encadreursA. HAOUAS et M. NOUREDDINE pour leur e�orts qu'ils ontfournis pour la réalisation de ce travail, nous tenons à remercieraussi Mr K. BELKADI de l'équipe du "laboratoire de rechercheen modélisation et évaluation des performances dans les systèmesde production" pour son aide.Nous remercions en�n tous ceux qui ont contribué, de loin ou deprès, à réaliser ce travail.�

Hakim FeghoulMohamed Mastour

1

Page 3: Compilateur orienté lambda calcul

Dédicace

�Je dédie ce modeste travail à ma famille, spécialement à mamère qui veille sur moi, à Mr A. HAOUAS qui ma fait décou-vrir une face cachée et très appétissante de la science, à tous mesamis qui m'ont appris beaucoup de choses sur la vie à travers lesexpériences partagées. Et spécialement, à tous ceux qui oeuvrentpour la science depuis la nuit des temps.�

Hakim FEGHOUL

2

Page 4: Compilateur orienté lambda calcul

�Computer Programming is both scienti�c and an engineering dis-cipline. As a scienti�c discipline, it seeks to establish genericprinciples and theories that can be used to explain or underpina variety of particular applications. As an engineering discipline,it construct substantial artefacts of software and hardware, seeswhere they fail and where they work, and develops a new theoryto underpin areas that are inadequately supported.�

Robert MilnerEloquently argues for this dual approach

in Computer Science, 1991

�Functional programming is a research area that o�ers an unu-sually close interplay between these two aspects�

Peyton JonesEloquently argues for this dual approach

in Computer Science, 1992

i

Page 5: Compilateur orienté lambda calcul

Table des matières

Introduction générale 1

1 Un langage fonctionnel type 31.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.2 Notion de typage . . . . . . . . . . . . . . . . . . . . . . . . . 41.3 Polymorphisme . . . . . . . . . . . . . . . . . . . . . . . . . . 51.4 Données de base . . . . . . . . . . . . . . . . . . . . . . . . . . 61.5 Fonctions . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 8

1.5.1 Dé�nition . . . . . . . . . . . . . . . . . . . . . . . . . 81.5.2 Fonctions strictes et fonctions non strictes . . . . . . . 91.5.3 Fonctions sans noms . . . . . . . . . . . . . . . . . . . 91.5.4 Composition des fonctions . . . . . . . . . . . . . . . . 9

1.6 Listes . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 101.6.1 Dé�nition . . . . . . . . . . . . . . . . . . . . . . . . . 101.6.2 Propriétés des listes . . . . . . . . . . . . . . . . . . . . 101.6.3 Opérations sur les listes . . . . . . . . . . . . . . . . . 101.6.4 Listes de compréhension . . . . . . . . . . . . . . . . . 13

1.7 Notion de patterns . . . . . . . . . . . . . . . . . . . . . . . . 151.7.1 Dé�nition . . . . . . . . . . . . . . . . . . . . . . . . . 151.7.2 Propriétés des patterns . . . . . . . . . . . . . . . . . . 151.7.3 Patterns à argument liste . . . . . . . . . . . . . . . . . 16

1.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 16

2 Lambda calcul et logique combinatoire 172.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . . . . 172.2 Notions théoriques . . . . . . . . . . . . . . . . . . . . . . . . 18

2.2.1 Description de base . . . . . . . . . . . . . . . . . . . . 182.2.2 lambda abstractions . . . . . . . . . . . . . . . . . . . 192.2.3 Lambda expressions . . . . . . . . . . . . . . . . . . . 192.2.4 Variables libres et variables liées . . . . . . . . . . . . . 202.2.5 Sémantique opérationnelle du lambda calcul . . . . . . 202.2.6 Sémantique dénotationnelle du lambda calcul : . . . . . 25

ii

Page 6: Compilateur orienté lambda calcul

2.2.7 Évaluation des expressions . . . . . . . . . . . . . . . . 272.3 Le lambda calcul enrichi . . . . . . . . . . . . . . . . . . . . . 29

2.3.1 Dé�nition . . . . . . . . . . . . . . . . . . . . . . . . . 292.3.2 pattern matching lambda abstraction . . . . . . . . . . 292.3.3 let et letrec expressions . . . . . . . . . . . . . . . . . . 302.3.4 Case expressions . . . . . . . . . . . . . . . . . . . . . 32

2.4 Logique combinatoire . . . . . . . . . . . . . . . . . . . . . . . 322.4.1 Combinateurs . . . . . . . . . . . . . . . . . . . . . . . 322.4.2 Supercombinateurs . . . . . . . . . . . . . . . . . . . . 32

3 Conception du compilateur 343.1 Systèmes de réécriture . . . . . . . . . . . . . . . . . . . . . . 34

3.1.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . 343.2 Transformation du langage source vers le lambda calcul enrichi 35

3.2.1 Exemple introductif . . . . . . . . . . . . . . . . . . . . 353.2.2 Dé�nition des schémas de translation d'un programme

fonctionnel . . . . . . . . . . . . . . . . . . . . . . . . . 373.2.3 Translation du pattern matching en lambda calcul enrichi 383.2.4 Translation des listes de compréhension . . . . . . . . . 46

3.3 Transformation du lambda calcul enrichi en lambda calcul or-dinaire . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 483.3.1 Transformation des lambda abstractions incluant le pat-

tern matching . . . . . . . . . . . . . . . . . . . . . . . 483.3.2 Transformation des let et letrec . . . . . . . . . . . . . 523.3.3 Transformation des case expressions . . . . . . . . . . . 573.3.4 L'Opérateur [] . . . . . . . . . . . . . . . . . . . . . . . 593.3.5 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . 59

3.4 Techniques d'évaluation . . . . . . . . . . . . . . . . . . . . . 593.4.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . 593.4.2 Environnement à l'évaluation . . . . . . . . . . . . . . 60

3.5 Supercombinateurs et lambda lifting . . . . . . . . . . . . . . 723.5.1 Utilité des supercombinateurs . . . . . . . . . . . . . . 733.5.2 Réduction par plusieurs arguments . . . . . . . . . . . 743.5.3 Implémentation des supercombinateurs . . . . . . . . . 743.5.4 Algorithme du lambda lifting . . . . . . . . . . . . . . 753.5.5 Stratégies d'évaluation des supercombinateurs . . . . . 77

3.6 Machine SK . . . . . . . . . . . . . . . . . . . . . . . . . . . . 783.6.1 SK Combinateurs . . . . . . . . . . . . . . . . . . . . . 783.6.2 S-Transformation . . . . . . . . . . . . . . . . . . . . . 783.6.3 I-Transformation : . . . . . . . . . . . . . . . . . . . . 793.6.4 K-Transformation . . . . . . . . . . . . . . . . . . . . . 803.6.5 Schéma �nal de la SK-Compilation . . . . . . . . . . . 81

iii

Page 7: Compilateur orienté lambda calcul

3.6.6 Avantages et inconvénients de la machine SK . . . . . . 823.7 G-Machine . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82

3.7.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . 823.7.2 Exemple d'exécution . . . . . . . . . . . . . . . . . . . 833.7.3 Syntaxe du langage source . . . . . . . . . . . . . . . . 853.7.4 État de la pile Durant l'exécution . . . . . . . . . . . . 853.7.5 Les Schémas de Compilation F et R . . . . . . . . . . . 873.7.6 Le Schéma de Compilation C . . . . . . . . . . . . . . 883.7.7 Schéma �nal de la G-compilation . . . . . . . . . . . . 913.7.8 Optimisation du G-code . . . . . . . . . . . . . . . . . 913.7.9 Fonctions prédé�nis . . . . . . . . . . . . . . . . . . . . 923.7.10 Extension du jeu d'instructions . . . . . . . . . . . . . 93

3.8 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95

4 Implémentation du compilateur 974.1 Objectifs de l'implémentation . . . . . . . . . . . . . . . . . . 974.2 Environnement de développement . . . . . . . . . . . . . . . . 98

4.2.1 Choix du langage . . . . . . . . . . . . . . . . . . . . . 984.2.2 Choix des outils de développement . . . . . . . . . . . 99

4.3 Structure de l'implémentation . . . . . . . . . . . . . . . . . . 1004.3.1 Types et structures de donnée utilisés . . . . . . . . . . 1014.3.2 Analyse lexicale et syntaxique . . . . . . . . . . . . . . 1024.3.3 Analyse statique . . . . . . . . . . . . . . . . . . . . . 1054.3.4 Gestion de la mémoire . . . . . . . . . . . . . . . . . . 1064.3.5 Compilation en supercombinateurs . . . . . . . . . . . 1084.3.6 Implémentation des primitives . . . . . . . . . . . . . . 1084.3.7 Instructions de la machine abstraite . . . . . . . . . . . 108

4.4 le �chier de la bibliothèque standard "stdlib" . . . . . . . . . . 1104.5 Interface utilisateur . . . . . . . . . . . . . . . . . . . . . . . . 111

Conclusion générale 114

A Règles d'écriture du langage 115

iv

Page 8: Compilateur orienté lambda calcul

Introduction générale

La programmation fonctionnelle ou programmation applicative est unstyle de programmation aussi ancien que la programmation impérative maisdont l'impact ne s'est considérablement accru que depuis quelques années[Lon89].

C'est une manière de programmer qui a vu le jour pour la première foisavec l'apparition du langage LISP (LISt Processor) [Hor88], destiné essen-tiellement à la manipulation des formules symboliques.

Ce dernier langage, qui a été développé par J. McCarthy entre 1956 et1958, avait comme tâche la représentation formelle des informations et l'im-plémentation des inférences logiques (Intelligence Arti�cielle).

LISP s'appuyait sur la théorie du lambda calcul qui a été inventée par A.Church durant les années 1930 dans l'intérêt de réduire la notion de fonctionen l'exprimant à partir de l'expression de sa valeur en un point.

Une autre approche a été développée par M. Schonk�nkel qui a fondé lalogique combinatoire ayant comme but l'élimination des variables des for-mules et l'utilisation de quelques opérateurs formels appelés "combinateurs"par H.B. Curry.

Ces deux approches présentent les caractéristiques des langages à hautniveau d'abstraction, ce qui a permis de considérer le lambda calcul et lalogique combinatoire comme un support théorique pour les autres langagesfonctionnels.

Notre travail consiste en l'écriture d'un compilateur d'un langage fonc-tionnel basé sur la théorie du lambda calcul et la logique combinatoire.

Notre approche inclut également l'évaluation des programmes écrit dansce langage.

Le présent mémoire est formé de quatre chapitres :� Le premier chapitre décrit la notion de programmation fonctionnelle

ainsi que le langage adopté.� Le deuxième chapitre introduit le lambda calcul et la notion des super-

combinateurs.� Le troisième chapitre analyse les di�érents passages à partir du code

source jusqu'à l'obtention d'un code bas niveau, Il traite aussi les tech-

1

Page 9: Compilateur orienté lambda calcul

2

niques d'évaluation du code produit.On soulèvera, au fur et à mesure, les problèmes liés à la translation ducode et à son évaluation.

� Le quatrième chapitre traite l'implémentation adoptée, la compositioninterne du compilateur et les interactions entre ses di�érentes compo-santes spéci�ques.

Page 10: Compilateur orienté lambda calcul

Chapitre 1

Un langage fonctionnel type

1.1 IntroductionLes langages fonctionnels sont les langages qui font référence aux fonctions

comme élément de traitement fondamental, ces fonctions sont appliquées àdes expressions appelées arguments de ces fonctions.

Autrement dit, un programme fonctionnel est une séquence de déclara-tions de fonctions en particulier.

Ces fonctions, en dehors des relations d'appels, sont indépendantes lesune des autres, elles ne manifestent pas donc des e�ets de bord sur lesvariables et elles peuvent être traitées et testées séparément.

Il existe deux groupes de langages fonctionnels :1. La classe des langages fonctionnels dits sans variables, une classe qui

a été fondée sur la logique combinatoire dé�nie par Schonk�nkel dèsles années 1920 et développée après par Haskell B. Curry.

2. La classe des langages dits avec variables, fondée sur le lambda calculdé�ni par Church (années 1930).

Dans la première classe, les fonctions sont décrites comme combinaisond'opérateurs ou combinateurs. On cite LML, Miranda , Orwell et Haskell quiest l'un des plus récents et les plus répandus de sa classe.

La deuxième classe, (langages avec variables), est fondée sur la théorie dulambda calcul, ce nom lui a été attribué en raison de la notation qu'on adoptepour la représentation des fonctions en cette théorie, on les note ainsi :

λ < V ariables liees > . < Corps de la fonction >

où <Variables liées> sont les paramètres de la fonction et <Corps de lafonction> désigne ce que fait cette fonction.

3

Page 11: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 4

Des structures conditionnelles et récursives ont été introduites pour sa-tisfaire les besoins liés à la conception d'une théorie puissante de calcul.

Pour évaluer ces fonctions, on dispose de quelques règles de conversion etde réduction jusqu'à obtenir leurs valeurs �nales.

Le lambda calcul permet de modéliser tous les aspects liés à la manipu-lation des fonctions et, par voie de conséquence, la sémantique des langagesfonctionnels. On énumère pour ces derniers l'ancien Lisp, le langage ML etHope, etc . . .

La programmation fonctionnelle o�re des avantages par rapport à la pro-grammation impérative, on peut en citer les points suivants :

� Un programme fonctionnel est un objet simple du point de vue ma-thématique [Lon89], il o�re la possibilité d'y opérer en utilisant destechniques de validation formelle et de transformation fondée sur desthéories puissantes.

� Le programmeur est dégagé du souci de contrôler l'ordre d'exécutiondes fonctions car cet ordonnancement est fait par le langage lui même,l'utilisateur est également libéré du problème de la gestion de la mé-moire qui est prise en charge par le langage ce qui évite les problèmesdus à cette gestion (déroutements, exceptions, bouclage indé�ni).

� Grâce à ses fonctions prédé�nies, un langage fonctionnel est facilementextensible et adaptable à de multiples contextes ou évolutions [Lon89].

Dans ce qui suit, on décrira un langage fonctionnel typique de la pre-mière classe [Hao99] [Pey87] [Jon91] , on présentera ses types de données quiy peuvent être manipulées, ses caractéristiques tirées des notions indispen-sables à implémenter pour tout langage fonctionnel de cette classe. Les règlesd'écriture décrivant la syntaxe du langage peuvent être consultées à l'annexejointe à ce mémoire.

1.2 Notion de typageUn typage est un partitionnement de l'ensemble des valeurs des expres-

sions. Un type est une classe de cette partition.Soit a un élément de type t, la notation adoptée pour représenter le type

de a est la suivante :a :: t

On distingue les types de données de base et les types composés dits aussistructurés.

Page 12: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 5

Types de données de baseLe langage est doté des types de données de base qui permettent de

construire ses expressions, ces types sont :� Le type entier, on le notera par (Int).� Le type réel, on le notera par (Float).� Le type booléen, on le notera par (Bool).� Le type caractère, on le notera par (Char).

Types composés (structurés)ce sont les types construits à l'aide des types de base, il comprennent :

� Le type uple noté comme (type 1,type 2,..., type n).

Exemple :Le type (Int, Int) est le type uple, plus précisément c'est du typepaire de valeurs.

� Le type fonction noté comme ( src -> dst )où :src type source de la fonctiondst son type destination

Exemple :Le type (Int -> Char) est le type fonction, son type source est Int,son type destination est Char.

� Le type liste noté comme [type].

Exemple :Le type [Bool] est le type liste de booléens.

1.3 Polymorphismeun type polymorphique est tout type de fonction tel que le type destina-

tion est déterminé, et le type source est quelconque.

Page 13: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 6

Exemple :Le type de fonction (a -> Int) tel que a est quelconque est un type

polymorphique.

1.4 Données de baseNombres

Ce type contient les entiers, les décimaux, les réels, on adopte la notationdécimale pour les nombres.

On dé�nis les opérations suivantes sur le type Int : + , - , * , / , ^(exponentiation), div, mod.

L'ordre d'association est par défaut à gauche, sauf pour l'exponentiationqui est associative à droite, on utilisera les parenthèses pour éviter touteambiguïté.

BooléensNous avons besoin aux deux valeurs de vérité pour pouvoir tester et com-

parer les expressions, ainsi les deux valeurs sont désignées par les expressions :True et False. Les prédicats usuels suivants sont également dé�nis en ce lan-gage : = , /= , < , > , >= , <=.

on utilise les opérateurs suivants :∧ : conjonction, ('et' logique)∨ : disjonction, ('ou' logique)not : négation, ('non' logique)

Le langage inclut aussi les expressions conditionnelles qui utilisent lesbooléens, elle sont de la forme :

if b then t else f

qui est une expression équivalente à t si b est de valeur True, et est équi-valente à f si b est de valeur False. b doit être de type Bool et t et f doiventêtre du même type l'expression est alors de ce même type.

Il est possible aussi, dans le cas où il y a plusieurs alternatives, de faireappel à un autre type d'expression, à savoir l'expression case qui a la syntaxesuivante :

Page 14: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 7

case exp of alts

où exp est une expression à évaluer et alts est une liste d'alternatives dela forme :

val -> resval une valeur qui peut être prise par l'expression expres résultat de l'expression case si exp est évaluée à val

Exemple :L'expression conditionnelle précédente peut être représentée en une ex-

pression case comme :

case b ofTrue -> tFalse -> f

Caractères et stringsl'ensemble des caractères ASCII fournit 128 symboles composés de signes

visibles et de caractères de contrôle, ces caractères constituent le type dedonnées char et sont donnés comme primitives, on se convient à les mettreentre quotes.

On donne deux fonctions primitives pour manipuler les éléments de typeChar :

� code :: Char -> Int , convertit un caractère en son nombre équi-valent ASCII

� decode :: Int -> Char , convertit un nombre en un caractère ASCII

il est possible de tester les caractères et de les comparer comme tout autretype, ceci de par leur code ASCII.

Un string est dé�ni comme une suite de caractères.On dé�nit aussi une fonction show qui prend une valeur arbitraire comme

argument et la convertit en une représentation imprimable, le but d'une tellefonction est de permettre de visualiser les valeurs qui ne sont pas des strings,ainsi cette fonction show a comme type (a -> String).

Page 15: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 8

UplesC'est une façon de combiner des types et de les apparier. Par exemple le

type (Int , Int). L'ordre sur les paires est l'ordre lexicographique, il estextensible à tout n-uple.

On dé�nis des fonctions primitives permettant d'extraire un élément d'unuple ainsi : On dé�nit en premier lieu les deux fonctions :

fst (x,y) = xsnd (x,y) = y

puis on généralisera sur tout uple :fstn (x1, x2,..., xn) = x1sndn (x1, x2,..., xn) = x2...nth (x1, x2,..., xn) = xn

1.5 Fonctions1.5.1 Dé�nition

Ce sont l'élément fondamental du langage, une fonction f appliquée à unargument a est notée comme suit :f a

Exemple :La fonction mathématique log appliquée à l'argument n est donc notée

comme : log nles opérateurs sont considérés comme des fonctions dont la syntaxe les dif-

férencie des autres fonctions telles qu'elles sont représentées dans le langage,les opérateurs ont la notation in�xe mais il est toutefois possible de convertirla syntaxe d'un opérateur en celle d'une fonction ordinaire et inversementgrâce à la notion suivante appelée notion de section.

Exemple :L'expression (x + y) peut être représentée comme (+) x y , ainsi (+)

est considéré ici comme une fonction à deux arguments.L'expression (mod x y) peut être représentée comme x `mod` y , ainsi

mod est considéré ici comme un opérateur.

Page 16: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 9

1.5.2 Fonctions strictes et fonctions non strictesUne fonction stricte a la propriété de produire un résultat indé�ni si

son argument est indé�ni, toute autre fonction est dite non stricte. Soit lafonction f dé�nie comme suit :f :: Int -> Intf x = a

l'évaluation de f(1/0) ne peut être faite si on cherche à connaître d'abordla valeur de l'argument, or on en a pas besoin car le résultat est constantquelque soit l'argument, la fonction f est non stricte. On discutera cettenotion plus en détail dans le chapitre suivant exposant la théorie du lambdacalcul.

1.5.3 Fonctions sans nomsLe langage permet l'utilisation des fonctions sans noms, leur syntaxe est

la suivante :

\ListArg -> expr

où : ListArg est une liste d'arguments atomique c-à-d de type qui n'estpas structuré et expr est une expression quelconque utilisant ces arguments.

Exemple :\x y-> x*y est une dé�nition d'une fonction sans nom.

Ce type de dé�nition trouvera son analogue dans la dé�nition des lambdaabstractions en lambda calcul.

1.5.4 Composition des fonctionsC'est un opérateur noté (.), l'expression :

(f.g) xest équivalente à l'expression f (g x) qui n'est autre que la composition ma-thématique ordinaire des fonctions. Son avantage est que certaines dé�nitionspeuvent être plus claires en adoptant cette notion.

Page 17: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 10

1.6 Listes1.6.1 Dé�nition

Une liste est une collection linéaire et ordonnée de valeurs, on adopteracomme notation pour les liste les quatre symboles suivants : [ ] , ..

Exemple :[1,2,3,4,5] , [1..5] sont deux exemples de la même liste.

1.6.2 Propriétés des listespropriété 1

Les éléments d'une liste ont tous le même type.

propriété 2Si les éléments d'une liste sont de type a alors la liste est de type [a]

propriété 3Une liste vide ne contient aucune valeur, ( notation : [ ] )

propriété 4Le type de la liste [ ] est polymorphique.

propriété 5Deux listes sont égales si elle contiennent les même valeurs dans le même

ordre.

1.6.3 Opérations sur les listesConcaténation

C'est une fonction qu'on note ++, elle produit en résultat une liste qui estla combinaison, dans le même ordre, des éléments des deux listes données enarguments à cette fonction.

Exemple :L'évaluation de [a,b,c] ++ [d] donne le résultat [a,b,c,d].

Page 18: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 11

LongueurUne opération, notée length, qui retourne le nombre d'éléments d'une

liste, on remarque que : si xs et ys sont deux listes alors :length ( xs ++ ys) = length xs + length ys

Exemple :length([a,b,c,d]) donne la valeur 4

Fonction headElle a pour argument une liste et comme résultat son premier élément.

Exemple :L'évaluation de : head([a,b,c,d]), donne le résultat a.

Fonction tailPrend une liste comme argument et produit une autre liste ne contenant

pas le premier élément de la liste argument.

Exemple :L'évaluation de : tail([a,b,c,d]), donne le résultat [b,c,d].

Fonction initElle a pour argument une liste et produit en résultat une liste après

suppression du dernier élément de la liste argument.

Exemple :L'évaluation de : init([a,b,c,d]), donne le résultat [a,b,c].

Fonction lastElle retourne le dernier élément d'une liste argument donnée.

Exemple :L'évaluation de : last([a,b,c,d]), donne le résultat d.

Page 19: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 12

Fonction takeC'est une fonction ayant deux arguments : un nombre entier n et une liste

xs, elle produit le segment initial de longueur n de la liste argument xs.

Exemple :L'évaluation de : take 2 [a,b,c,d], donne le résultat [a,b].

Fonction dropElle prend les même arguments que la fonction take, elle produit une liste

après suppression des n premiers éléments de xs.

Exemple :L'évaluation de : drop 2 [a,b,c,d], donne le résultat [c,d].

Fonction takewhileElle a comme syntaxe : takewhile p xs , où : p est un prédicat et xs est une

liste, elle prend le plus long segment initial de xs dont les éléments satisfontle prédicat p.

Exemple :L'évaluation de : takewhile odd [1,2,3,4], donne le résultat [1,3].

Fonction dropwhileMême syntaxe avec la di�érence qu'elle produit une liste après suppression

du plus long segment initial dont les éléments satisfont le prédicat p.

Exemple :L'évaluation de : dropwhile odd [1,2,3,4], donne le résultat [2,4].

Fonction reverseInverse l'ordre des éléments d'une liste �nie.

Exemple :L'évaluation de : reverse [1,2,3,4], donne le résultat [4,3,2,1].

Page 20: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 13

Fonction zipElle prend une paire de liste comme arguments et produit une liste de

paires composées des éléments des deux listes.

Exemple :zip ([0,1,2],[1..3]) donne : [(0,1),(1,2),(2,3)]

1.6.4 Listes de compréhensionLeur syntaxe est tirée de la représentation des ensembles en mathéma-

tiques, une liste de compréhension est dé�nie par la syntaxe suivante :[ < expression > | < qualifier > ,..., < qualifier > ]

où :< expression > est une expression arbitraire,< qualifier > est une expression à valeur booléenne ou un générateur.

Un générateur a la forme syntaxique suivante :< variable > <- < liste >

Exemple :L'évaluation de

[(a,b) | a <- [1..3], b <- [1..2]]produit la liste suivante :

[(1,1),(1,2),(2,1),(2,2),(3,1),(3,2)]

i. Utilisation des listes de compréhension pour la dé�nitions desfonctions

Les fonctions peuvent être dé�nies par des listes de compréhension :

Exemples :� La fonction donnant les diviseurs d'un nombre entier n :

divisor n = [d | d <- [1..n], n `mod` d == 0]

� La fonction pyth qui retourne une liste de tous les triangles pythago-réens tels que la somme des cotes est inférieure à n :

Page 21: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 14

pyth n = [(a,b,c)| a <- [1..n],b <- [1..n],c <- [1..n],a+b+c <= n,

square a + square b == square c ]

ii. Autres fonctions sur les listesLes fonctions foldr et foldl

La plupart des fonctions vues jusque là, retournent des listes comme re-sultat. Les fonctions fold sont plus générales dans la mesure où elle conver-tissent les listes en d'autres sortes de valeurs, une dé�nition informelle defoldr donne :

foldr f a [x1, x2, . . . , xn] = f x1 (f x2 (. . . (f xn a) . . . ))

même chose pour foldl :

foldl f a [x1, x2, . . . , xn] = f (. . . (f (f a x1) x2) . . . ) xn

plusieurs fonctions sur les liste peuvent être dé�nies grâce aux fonctionsfold

Exemple :sum = foldr (+) 0product = foldl (*) 1concat = foldr (++) []and = foldr (/\) Trueor = foldr (\/) False

Fonction mapC'est une fonction qui applique une fonction à tous les éléments d'une

liste, d'où :map f xs = [ f x | x <- xs]

Page 22: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 15

Fonction �lterUne fonction qui a comme argument une liste xs et un prédicat p et

comme résultat la sous liste des éléments véri�ant p :

filter p xs = [x | x xs , p x]

1.7 Notion de patterns1.7.1 Dé�nition

Les patterns servent, entre autres, à dé�nir les fonctions du langage,l'exemple suivant met cette notion en évidence,

La fonction prédécesseur peut être dé�nie comme suit :pred 0 = 0pred n = n-1

On dit que n est mis en matching avec la valeur entière de n. La notiondu pattern matching sera étudiée plus en détail au cours des chapitres quisuivent.

1.7.2 Propriétés des patternsi. Exhaustivité des patterns

Deux patterns sont exhaustifs si et seulement s'ils couvrent tous l'en-semble de dé�nition des variables.

ii. Disjonctivité des patternsDeux patterns sont dits disjoints si aucune variable ne correspond à deux

patterns à la fois.

iii. Styles de dé�nition du pattern matchingIl est possible de dé�nir le pattern matching par équations ou par condi-

tions, la dé�nition suivante d'une fonction cond illustre ces deux possibilités :cond True x y = xcond False x y = y

C'est le style équationnel, le style conditionnel donne, pour la même fonc-tion :

Page 23: Compilateur orienté lambda calcul

CHAPITRE 1. UN LANGAGE FONCTIONNEL TYPE 16

cond p x y = x, if p == True= y, if p == False

1.7.3 Patterns à argument listeLa notion du pattern matching déjà dé�nie sert aussi à exprimer des

fonctions à argument liste, pour cela on dé�nit un opérateur constructeur deliste qu'on note (:). L'opérateur (:) ou (Cons) insère une valeur commenouveau premier élément dans une liste, ainsi :1 : 2 : [3,4]

donne :[1,2,3,4]

On dé�nit les fonctions à argument liste plus aisément, l'exemple suivantmontre celà :

single est la fonction qui détermine si une liste donnée contient un seulélément ou nonsingle [] = Falsesingle [x : []] = Truesingle [x : y : xs] = False

1.8 ConclusionCe chapitre a traité la partie essentielle de la couche supérieure du compi-

lateur, c'est-à-dire le langage source introduit par l'utilisateur à la machine,le processus de compilation sera décrit en détail plus loin, on donnera en cequi suit les fondements théoriques du compilateur.

Page 24: Compilateur orienté lambda calcul

Chapitre 2

Lambda calcul et logiquecombinatoire

2.1 IntroductionLe lambda calcul est un langage particulier qu'on utilise comme intermé-

diaire entre une syntaxe quelconque d'un langage fonctionnel et sa représen-tation bas niveau. Un programme fonctionnel étant composé d'une suite dedé�nitions et une expression à évaluer est donc transformé en une expressionen lambda calcul qu'on évalue automatiquement après conversion en lan-gage bas niveau. On exposera la théorie du lambda calcul plus explicitementpour pouvoir décrire le processus d'exécution des expressions du langage. Lescombinateurs et notamment les supercombinateurs seront dé�nis ici commenotion théorique, leur manipulation fera l'objet du chapitre suivant décrivantle processus de compilation.

Exemple :Soit l'expression suivante écrite en syntaxe lambda calcul :

(∗ 2 4)

cette dernière notation veut dire : "la fonction * appliquée à deux argu-ments qui sont ici 2 et 4", ce qui produira après évaluation la valeur 8.

Nous voyons clairement que la notation pré�xée coïncide exactement surla notion d'application de fonction.

On l'adoptera pour les expressions lambda calcul.

Exemple :Soit l'expression suivante :

17

Page 25: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 18

(/ (∗ 2 4 ) (+ 1 1 ))

contrairement à l'exemple précédant nous voyons ici une expression com-posée de deux autres expressions plus simples, avec lesquelles on commenceral'évaluation ce qui produira :

(/ 8 (+ 1 1 ))

dans un premier temps puis

(/ 8 2)

dans un second temps et �nalement le résultat qui est alors la valeur 4.Notons ici qu'il y a des expressions plus simples à évaluer que d'autres, cesexpressions sont appelées expressions réductibles ou plus pratiquement redex.

2.2 Notions théoriques2.2.1 Description de baseCurry�cation

L'application des fonctions à leurs arguments est déterminée par ce quisuit :

� Si f est à un seul argument x l'expression est écrite f x.� Si f possède plusieurs arguments l'expression s'écrit :

(. . . (((f x1) x2) x3) . . . xn)

Cette dernière notation appelée notation de Schonk�nkel indique : "lafonction f appliquée à l'argument x1 ce qui produit une fonction appliquéeà l'argument x2 . . . qui produit une fonction appliquée à l'argument xn ",de cette façon toute fonction peut être évaluée en prenant un seul de sesarguments, une propriété utilisée par Curry d'où le nom curry�cation.

On décrira en ce qui suit les constructions adoptées en la syntaxe dulambda calcul.

Page 26: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 19

Constantes et primitivesConcrètement, on est amené à considérer certains ensembles comme pré-

dé�nis en lambda calcul et qui sont :

� L'ensemble des constantes arithmétiques 0,1,2,. . .� L'ensemble des constantes logiques True, False� L'ensemble des constantes caractères 'a','b','c',. . .� L'ensemble des primitives arithmétiques + , - , * , /� L'ensemble des primitives logiques and, or, not

Ces constructions sont étendues par une fonction conditionnelle if dé�niepar les règles suivantes :

if True ET EF → ET

if False ET EF → EF

Cet ensemble est également étendu par les trois constructeurs de donnéessuivants : cons, head, tail dé�nis par :

head (cons a b ) → a

tail (cons a b ) → b

2.2.2 lambda abstractionspour dé�nir des fonctions en lambda calcul autres que celles prédé�nies,

une manière de les construire a été introduite appelée lambda abstraction,elle a la composition suivante :

λparamtre formel . corps de la fonction

Exemples :(λ x.+ 4 x) , (λ y./ y 2) sont des lambda abstractions.

2.2.3 Lambda expressionsIl s'agit de toute expression écrite en syntaxe du lambda calcul, par consé-

quent on la dé�nit par la notation BNF suivante :

Page 27: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 20

< exp > : := < constante > constantes prédé�nies| < variable > noms de variables| < exp > < exp > applications| λ < variables >.< exp > lambda abstractions

2.2.4 Variables libres et variables liéesC'est une notion importante pour décrire le processus d'évaluation des

lambda expressions, nous allons commencer par donner un exemple sur celà :

Soit l'expression : (λx.(f x y)) a

Il est indispensable ici de connaître la valeur de y pour pouvoir évaluerl'expression. Or ce n'est pas le cas pour x car celle ci est liée par λ, ainsi ondit que x est variable liée et y est variable libre. Les dé�nissions suivantesgénéralisent cette notion :

Soient E et F deux lambda expressions :

Dé�nition 11. x occure librement dans x2. x occure librement dans (E F ) si et seulement si x occure librement

dans E et dans F3. x occure librement dans λy.E si et seulement si x et y sont distinctes

et x occure librement dans E

Dé�nition 21. x occure liée dans (E F ) si et seulement si x occure liée dans E ou

dans F2. x occure librement dans λy.E si et seulement si

( (x et y sont identiques et x occure librement dans E) ou (x occureliée dans E) )

2.2.5 Sémantique opérationnelle du lambda calculConversions en lambda calcul

Le lambda calcul dispose de trois types de conversions permettant de pas-ser d'une lambda expression à une autre, ces trois types sont :

Page 28: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 21

� la bêta conversion (β)� la alpha conversion (α)� la eta conversion (η)

i. Bêta conversionil s'agit de l'application d'une lambda abstraction à un argument donné,

ainsi l'expression :(λx.(f x y) a) est composée de la lambda abstraction λx.(f x y) et de

l'argument a et est évaluée ainsi (f a y), c'est-à-dire en remplaçant, dans lecorps de la lambda abstraction, l'occurrence libre du paramètre formel x para. Le résultat d'application d'une lambda abstraction à un argument est uneinstance du corps de la lambda abstraction dans lequel les occurrences libresdu paramètre formel sont remplacées par des copies de l'argument.

Ce passage est appelé bêta conversion ou bêta réduction et est noté pourcet exemple :

(λx.(f x y)) a β−→ (f a y)

Si les noms des paramètres formels ne sont pas uniques ceci donne lieu àune éventuelle ambiguïté, l'exemple suivant illustre celà :

(λx.(λx.+ (∗ x 1))x 2) 5

La question ici est :"quelle occurrence de x doit-on remplacer par l'argu-ment donné ?".

On peut résoudre le problème en substituant l'occurrence libre de x dansE où l'expression citée ci-dessus est sous la forme λx.E, la substitution iciest permise car x dans λx.E est variable liée.

L'expression ci-dessus s'évalue donc par la succession suivante :

(λx.(λx.+ (∗ x 1)) x 2) 5→ λx.+ (∗ x 1) 5 2

→ +(∗ 5 1) 2

→ +( 5 2)

→ 7

On remarque qu'on procède par l'évaluation de la lambda abstraction laplus externe.

ii. Alpha conversionC'est un renommage d'un paramètre formel, comme exemple très simple

on donne le passage de l'expression (λx.+ x a) vers l'expression (λy.+ y a).

Page 29: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 22

L'utilité d'une telle conversion est d'éviter les risques d'ambiguïté dans uneexpression pendant son évaluation.

iii. Eta conversionOn la dé�nit comme le passage de la lambda abstraction suivante : λx.(f x)

à l'expression f avec la condition que f soit une fonction et que f ne possèdepas d'occurrences libres de x.

(λx.f x) η−→ f

L'exemple suivant illustre ce type de conversions :(λx.+ a x) et (+a) qui sont eta convertiblesl'utilité des eta conversions est qu'ils rendent possible l'élimination, lors

du processus d'évaluation, de certaines lambda abstractions dites lambdaabstractions redondantes.

Réduction des lambda expressionsC'est l'opération fondamentale du processus d'évaluation des redex (ex-

pressions réductibles), jusqu'à ce que celles-ci ne �gurent plus dans l'expres-sion �nale appelée forme normale de ladite lambda expression. Étant donnéeune expression à plusieurs redex, la réduction peut se faire de plus qu'unemanière, soit l'expression suivante :

(+ (∗ 2 5) (∗ 6 3 ))

cette dernière peut être évaluée ainsi :

(+ (∗ 2 5) (∗ 6 3 ))→ (+ 10 (∗ 6 3))

→ (+ 10 18)

→ 28

or il existe une deuxième manière :

(+(∗ 2 5) (∗ 6 3))→ (+(∗ 2 5 ) 18)

→ (+ 10 18)

→ 28

De plus changer l'ordre de réduction peut provoquer des résultats di�é-rents, l'exemple suivant montre celà :

Page 30: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 23

(λx.a) (λx.xx) (λx.xx)en procédant par réductions à gauche on obtiendra la séquence suivante :

(λx.a) (λx.xx) (λx.xx) → a

or, si on réduit de droite on produira la séquence :

(λx.a) (λx.x x) (λx.x x)→ (λx.a) (λx.x x) (λx.x x)

→ (λx.a) (λx.x x) (λx.x x)

→ . . .

qui est une séquence indéterminée, l'expression ne peut plus être doncévaluée.

Le problème qui se pose donc est de chercher si une expression mène àdi�érentes formes normales par certains choix de réduction.

La solution du problème est connue sous le nom de théorèmes de Church-Rosser.

Théorème 1Si E1 et E2 sont deux expressions interconvertibles alors il existeune expression E telle que E1 → E et E2 → E. De ce fait toutexpression n'est convertible en deux formes normales distinctes.

Théorème 2Si E1 admet E2 comme forme normale alors il existe un ordrenormal de réduction de E1 en E2.

L'ordre normal de réduction spéci�e la réduction du redex le plus à gauchedu niveau supérieur.

RécursivitéLe but plus tard est de translater tout programme fonctionnel en lambda

calcul, or l'une des caractéristiques les plus importantes des langages fonc-tionnels est la récursion, elle nous donne certains pouvoirs d'expressions, cecipousse à douter de la possibilité qu'il y ait une manière de translater la récur-sion en lambda calcul, car ce dernier nous paraît manquant de toute notionqui correspond à la récursion.

En ce qui suit nous allons montrer que le lambda calcul est capable d'ex-primer la récursivité et sans aucune extension ce qui prouve sa puissanced'expression.

Page 31: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 24

La fonction YConsidérons l'exemple sur la fonction factorielle dé�nie comme suit :

fac = (λn. if (= n 0) 1 (∗ n (fac (− n 1))))

Cette dé�nition repose sur la possibilité de nommer une lambda abstrac-tion et de mentionner ce nom aussi dans son corps mais le problème est queles lambda abstractions sont des fonctions sans nom.

Pour simpli�er cela, on procède à l'écriture d'une bêta-abstraction surfactorial d'où :

fac = (λfac.(λn. if (= n 0) 1 (∗ n (fac (− n 1))))) fac

Ce qui donne :fac = H (fac)

où :H = (λfac.(λn. if (= n 0) 1 (∗ n (fac (− n 1)))))

On dit que H est un point �xe de fac.Dans ce cas on peut mettre fac = Y H , où Y est une fonction qui donne

pour chaque fonction H son point �xe, (ici fac), on remarque donc que :

Y H = H (Y H)

Si on essaye l'exemple fac 1, on obtiendra la séquence suivante :

fac 1→ Y H 1

→ H (Y H) 1

→ (λfac.(λn. if (= n 0) 1 (∗ n (fac (− n 1))))) (Y H) 1

→ (λn. if (= n 0) 1 (∗ n ((Y H) (− n 1)))) 1

→ if (= 1 0) 1 (∗ 1 ((Y H) (− 1 1)))

→ ∗ 1 ((Y H) 0)

→ ∗ 1 (H (Y H) 0)

→ ∗ 1 ((λn. if (= n 0 ) 1 (∗ n ((Y H) (− n 1)))) 0)

→ ∗ 1 (if (= 0 0) 1 (∗ 0 ((Y H) (− 0 1))))

→ ∗ 1 1

→ 1

Page 32: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 25

Il est possible de dé�nir Y comme une lambda abstraction simple par :

Y = (λh.(λx.h (x x)) ((λx.h (x x)))

et il est simple à véri�er dans :

Y H = H (Y H)

Y est appelé aussi combinateur du point �xe.

2.2.6 Sémantique dénotationnelle du lambda calcul :Dé�nition

Dans la sémantique dénotationnelle on considère la fonction comme étantun ensemble de paires arg-val où :arg est l'argument de la fonctionval valeur de la fonction pour cet argument

Pour donner une sémantique dénotationnelle au lambda calcul il faut at-tribuer une valeur (dé�nie comme étant un objet purement mathématique)à chaque expression (dé�nie comme étant un objet syntaxique formé à par-tir de la grammaire du lambda calcul), et ceci en dé�nissant une fonctionnommée Eval.

Finalement, il convient de considérer le lambda calcul comme un systèmeformel manipulant des symboles syntaxiques.

La fonction EvalCette fonction est dé�nie pour exprimer sémantiquement les expressions

en donnant leurs valeurs mathématiques comme résultat.par exemple :

Eval [+ 3 4] = 7

On procédera de ce fait en se référant à la syntaxe (grammaire) du lambdacalcul qui donne les di�érentes formes possibles d'une expression E :

Cas 1, ( E est une variable x ) :la valeur d'une variable est donnée par son contexte englobant et pour

cela on enrichie Eval d'un extra paramètre p, qui donne l'information contex-tuelle, (p→ environnement).

Page 33: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 26

Expression Valeur-

Eval

p est une fonction, à qui on donnera une variable x, et elle retourne savaleur dans l'environnement, ainsi :

Eval [x] p = (p x)

Cas 2, (E est une application) :Il est considéré que la valeur de E1 E2 est la valeur de E1 appliquée à la

valeur de E2 c'est à dire :

Eval[E1 E2] p = (Eval[E1] p) (Eval[E2] p)

Cas 3, (E est une abstraction) :Le résultat sera une fonction mais on ne peut le dé�nir qu'après l'appli-

cation de cette fonction à un argument a, d'où :

(Eval [λx.E] p) a

la valeur d'une lambda abstraction, appliquée à un argument, est la valeurde son corps, dans un contexte où le paramètre formel est liée à l'argument :

(Eval [λx.E] p) a = Eval [E] p[x ← a]

p[x ← a] est une simple notation qui signi�e l'environnement p augmentéde [x ← a].

Cas 4, (E est une constante ou une fonction prédé�nie) :Dans ce cas, on considère la valeur mathématique de la constante ou la

fonction prédé�nie qui a été introduite. par exemple :

Eval[6] = 6Eval[True] = TrueEval[∗] a b = a ∗ b. . .

Page 34: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 27

2.2.7 Évaluation des expressionsFonctions strictes et fonction non strictes en lambda calcul

Une fonction est dite stricte si on est sûr qu'elle a besoin aux valeurs deses arguments.

Dans ce cas, si l'évaluation de l'argument ne termine pas alors l'évaluationde la fonction ne termine pas non plus.

Par exemple, la fonction (λx.λy.+ x y) est stricte car elle a besoin à sesarguments pour être évaluée.

Autrement, on dit que la fonction est non stricte (ou paresseuse).

Exemple :la fonction (λx. 3) n'a pas besoin d'un argument, quelque soit cet argu-

ment, pour être évaluée, et de ce fait on conclue des di�érentes stratégiesd'évaluation.

Évaluation par valeurC'est la technique la plus classique, elle consiste à calculer les valeurs

des arguments d'une fonction puis substituer ces valeurs dans le corps de lafonction avant d'évaluer celle-ci.

Cette technique présente l'avantage d'être assez facile à implémenter, enrevanche, elle peut conduire à l'évaluation des arguments qui ne sont pasutilisés dans le corps de la fonction ou bien à une évaluation in�nie. Onnote que c'est la méthode utilisée dans les langages impératifs,( tels que C,Pascal,. . . ).

Exemples :� L'évaluation par valeur de l'expression suivante :

(λx.3) ((λx.x x) (λx.x x))

implique un bouclage in�ni, alors qu'elle a comme valeur 3

� La fonction :

(λx.λy.if (= x 0) x y)

calcule les valeurs de ses deux arguments alors qu'elle n'utilise que l'und'eux.

Page 35: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 28

Évaluation par nomCette technique consiste à substituer l'argument (sans qu'il soit évalué),

dans le corps de la fonction.

Exemples :� L'évaluation de : (λx.1) (1/0), donne 1� L'évaluation de :

(λx.(+ x x)) (+ 1 2)→ + (+ 1 2) (+ 1 2)

→ + 3 (+ 1 2)

→ + 3 3

→ 6

L'avantage de cette technique est que seuls les arguments dont on a besoinsont calculés, mais son inconvénient est que l'argument peut être évalué plusqu'une fois.

Évaluation par besoinCette stratégie consiste à combiner les avantages des deux méthodes citées

précédemment, les paramètres sont passés selon le mécanisme de passage parnom, mais une fois calculés, leurs valeurs sont conservées transformant lepassage des arguments en passage par valeur.

Exemple :L'évaluation de :

(λx.(∗ x x)) (+ 1 2)→ ∗ (+ 1 2) (+ 1 2)

→ + 3 3

→ 9

Les langages utilisant la première technique sont les dits langages stricts,ceux qui utilisent l'évaluation par besoin sont dits paresseux (lazy).

On remarque que pour une réduction optimale on doit procéder à unordre normal de réduction.

Page 36: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 29

2.3 Le lambda calcul enrichi2.3.1 Dé�nition

C'est une extension du lambda calcul ordinaire (dit aussi simple) déjàdécrit, cette extension consiste en l'ajout des extra constructeurs suivants :

� pattern matching lambda abstractions� let expressions et letrec expressions� case expressions� l'opérateur in�xe [], (fat bar)

On traitera seulement les trois premiers extraconstructeurs, le derniersera traité plus concrètement dans le chapitre suivant et ce dans la partieconcernant le pattern matching puisqu'ils est lié à ce dernier.

2.3.2 pattern matching lambda abstractioni. Dé�nition

elle a pour syntaxe :

λp.E

où p est un pattern, c'est à dire : p est soit une variable, soit une constante,soit un type structuré de la forme (c p1 p2 . . . pn), avec c un constructeur detype et p1 p2 . . . pn sont des patterns.

Exemples :dans l'expression

(λ1.+ 1 2) 1

le λ1 est mis en matching avec l'argument 1, dans ce cas l'expression estévaluable. Par contre l'expression :

(λ1.+ 1 2) 2

n'est pas évaluable.

Page 37: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 30

2.3.3 let et letrec expressionslet expressions simplesi. Dé�nition

elle a pour syntaxe :

let v = B in E

où :v est une variable , B et E sont des expressions du lambda calcul enrichi.

Exemples :� L'évaluation de let x = 3 in (∗ x x), produit (∗ 3 3), ce qui donnera 9� L'évaluation de (+ 1 (let x = 3 in (∗ x x))) produit + 1 (∗ 3 3), puis

(+ 1 9) ce qui donnera 10

Les expressions let peuvent être aussi imbriquées :

let x = 3 in (let y = 4 (∗ x y))→ let x = 3 in (∗ x 4)

→ ∗ 3 4

→ 12

ce dernier exemple peut être écrit :

let x = 3

y = 4

in ∗ x y

ii. Sémantique d'une let expression simpleIl est possible de dé�nir par une lambda abstraction, où :

(let v = B in E) ≡ ((λv.E) B)

Page 38: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 31

letrec expressions simplesi. Dé�nition

elles ont la même syntaxe que les let expressions simples :

letrec v1 = E1

v2 = E2

...vn = En

in E

Où les vi sont des variables, et Ei sont des expressions du lambda calculenrichi. La di�érence entre let et letrec est que les variables vi dans une letrecexpression peuvent �gurer dans les expressions Ei aussi.

Exemple :Considérons l'expression suivante :

letrec fac = λn.if (= n 0) 1 (∗ n (fac (− n 1)))

in fac 4

le résultat après application de la fonction dé�nie récursive fac à l'argu-ment 4, sera donc 24.

Il est possible d'imbriquer les letrec aussi, la récursion mutuelle est pos-sible dans les letrec c'est-à-dire l'exemple suivant :

letrec f = . . . f . . . g . . .

g = . . . f . . .

in . . .

ii. Sémantique d'une letrec expression simpleL'opérateur Y ici est très utile, ainsi la sémantique d'une telle expression

sera équivalente à l'expression suivante :

(let v = Y (λv.B) in E)

on note que l'utilisation de Y rend la dé�nition non récursive, ce quipermet d'utiliser une let.

Page 39: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 32

2.3.4 Case expressionsLa forme générale d'une case expression est la suivante :

case v of

p1 → E1

p2 → E2

...pn → En

où :v est une variable, E1, . . . , En , sont des expressions, les pi sont des

patterns distincts.Ces notions seront étudiées plus en détail lors de la description des sché-

mas de translation et l'introduction de l'opérateur [] au chapitre suivant.

2.4 Logique combinatoireLa notion des combinateurs et des supercombinateurs à été introduite à

cause du problème des variables libres dans une lambda expression lors deson évaluation, une variable libre ne peut avoir la valeur d'un argument etdonc n'est pas évaluée.

2.4.1 CombinateursUn combinateur est une lambda expression qui ne contient pas d'occur-

rences de variables libres.

2.4.2 SupercombinateursDé�nition

On appelle supercombinateur toute lambda expression de la forme :

λx1.λx2 . . . λxn.E

où :� E n'est pas une lambda abstraction.� E n'admet pas de variables libres.� Toute lambda abstraction dans E est un supercombinateur.

Page 40: Compilateur orienté lambda calcul

CHAPITRE 2. LAMBDA CALCUL ET LOGIQUE COMBINATOIRE 33

� n ≥ 0on dit que n est l'arité du supercombinateur.

Exemples :(λx. ∗ x x) et (λx.λy.y x) sont des supercombinateurs.

L'application d'un supercombinateur à ses arguments a le nom du redex -supercombinateur, elle est similaire à celle des autres lambda abstractions.

Arité des supercombinateursi. Supercombinateur d'arité non nulle

Ils constituent l'unité de la compilation, on peut compiler une séquencede code pour eux car il ne contiennent pas des variables libres.

ii. Supercombinateurs d'arité zéro (C.A.F)Ce sont les constantes (CAF : Constant Applicative Form ). Les CAF

peuvent être considérés comme des fonctions, on ne les instancie pas pendantla compilation.

ConclusionAprès avoir dé�ni, dans ce chapitre, l'assise théorique du compilateur, à

savoir la théorie du lambda calcul et la notion de combinateurs et des super-combinateurs, on décrira les di�érentes stratégies et étapes de compilationavec introduction, au fur et à mesure, de quelques notions supplémentairesnécessaires à la compréhension du processus.

Page 41: Compilateur orienté lambda calcul

Chapitre 3

Conception du compilateur

IntroductionLa compilation est le processus qui permet de passer d'un programme

écrit en une certaine forme vers un programme équivalent ayant une autreforme plus simple à traiter par la machine.

On entend ici par simplicité le fait que le programme cible soit exécutépar de simples opérations d'évaluation.

De ce fait la compilation ici n'est qu'une succession de réécriture du code.On décrira, en ce qui suit, les di�érentes réécritures ou translations du

code, à savoir translation vers le lambda calcul enrichi, puis vers le lambdacalcul ordinaire, puis la représentation en supercombinateurs et en�n les stra-tégies d'évaluation des expressions résultantes.

3.1 Systèmes de réécriture3.1.1 Introduction

Cette compilation consiste en l'écriture du code source en un code écriten lambda calcul.

Nous allons démontrer, par la suite, qu'à chaque expression du code sourcecorrespond une expression sémantiquement équivalente du code lambda cal-cul.

La sémantique du lambda calcul étant déjà fondée [Bar84], ceci impliqueautomatiquement que le langage source en question est bien fondé, c'estprécisément l'approche adoptée par la sémantique dénotationnelle [Pey87].

De plus, et comme conséquence, nous allons donner une manière de trans-lation des expression de haut niveau en leurs équivalentes en lambda calculenrichi.

34

Page 42: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 35

Programme fonctionnel

λ calcul ordinaire

λ calcul enrichi-

?

Fig. 3.1 � Translations jusqu'au lambda calcul

Une fois le programme lambda calcul enrichi obtenu il sera réécrit à sontour en un équivalent lambda calcul ordinaire, ce dernier fera l'objet de l'exé-cution désignée ici par le terme évaluation.

Ainsi le schéma de la �gure 3.1 récapitule le processus de compilationpour l'évaluation d'une certaine expression.

Une première question qui se pose ici est : "pourquoi ne pas translaterle programme fonctionnel source directement en lambda calcul ordinaire ?",la réponse est que le processus de translation sera extrêmement compliqué,c'est pourquoi nous avons adopté l'approche "pas à pas", de plus nous seronsen mesure de dé�nir facilement la sémantique dénotationnelle du langagefonctionnel comme étant équivalente à celle du lambda calcul enrichi.

3.2 Transformation du langage source vers lelambda calcul enrichi

3.2.1 Exemple introductifUn programme fonctionnel s'écrit sous forme d'un ensemble de dé�nitions

associées à une expression évaluable.

Soit le programme suivant :

square n = n * n----------------2 * square 5

Page 43: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 36

informellement, la translation, en lambda calcul enrichi, de ce programmedonnera lieu à l'expression suivante :

let square = λn. ∗ n n

in(∗ 2 (square 5))

Ici l'expression square n = n*n a engendrée la dé�nition d'une lambdaabstraction (chapitre 2) (λn. ∗ n n), il en est de même pour l'expression2 * square 5. On introduira donc pour mieux décrire le processus de trans-lation une fonction, TD, ayant comme argument l'expression du code sourceet produisant comme résultat une lambda expression (chapitre 2), on écriraaisément :

TD[square n = n ∗ n

]≡ square = λn. ∗ n n

De même la deuxième transformation qui s'est produite à l'expressiond'évaluation est aussi assimilable à une fonction, TE, et s'écrit donc :

TE[2 ∗ square 5

]≡ ∗ 2 (square 5)

En général on adoptera une notation pour translater un programme source,ainsi le programme suivant :

Definition1

Definition2

...Definitionn

�����Expression a evaluer

(3.1)

engendre, après translation, le code suivant :

Page 44: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 37

letrec TD[Definition1

]TD

[Definition2

]...

TD[Definitionn

]in TE

[Expression a evaluer

](3.2)

En appliquant cela sur l'expression ci dessus on aura :

TD[square n = n ∗ n

]≡ square = λn. ∗ n n

TE[2 ∗ square 5

]≡ ∗ 2 (square 5)

3.2.2 Dé�nition des schémas de translation d'un pro-gramme fonctionnel

On distinguera les deux schémas de translation notés ci-dessus, les sché-mas TE et TD :Le schéma de translation TE prend comme argument une expression source

et produit comme résultat une lambda expression, ainsi on dé�nira TEpour les constantes, les variables, les applications, comme suit :Translation des constantes :

TE[k

]≡ k (3.3)

où k est une constante.

Translation des variables :

TE[v

]≡ v (3.4)

où v est une variable.

Page 45: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 38

Translation des applications :

TE[E1 E2

]≡ TE

[E1

]TE[E2

](3.5)

TE[E1 infix E2

]≡ TE

[infix

]TE[E1

]TE[E2

](3.6)

TE[E1 ‘v‘ E2

]≡ TE

[v

]TE[E1

]TE[E2

](3.7)

où infix est un opérateur in�xe et ‘v‘ est une fonction quelconque.Le schéma de translation TD prend comme argument les dé�nitions du

langage source et produit en résultat des dé�nitions letrec, en généralon translate les dé�nitions des variables et des fonctions :Translation des dé�nitions des variables :

TD[v = E

]≡ v = TE

[E

](3.8)

où E est une expression du langage sourceTranslation des dé�nitions des fonctions :

TD[f v1 v2 . . . vn = E

]≡ f = λv1 v2 . . . vn.TE

[E

](3.9)

où f v1 v2 . . . vn sont des variables et E une expression source.

3.2.3 Translation du pattern matching en lambda calculenrichi

i. IntroductionLa notion du pattern matching est l'une des extensions les plus impor-

tantes implémentées dans les langages fonctionnels tels que Haskell, Miranda,Orwell, ML. Le pattern matching a permis de renforcer le pouvoir de l'expres-sion dans ces langages (section 1.7 ) et dans cette partie nous allons expliquercomment on arrive à translater les dé�nitions qui utilisent le pattern matchingen lambda calcul enrichi.

Page 46: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 39

ii. dé�nition d'un patternUn pattern est :� soit une variable v ou une constante k (numériques, booléennes, carac-

tère . . . ).� soit une donnée structuré de la forme (c p1 . . . pn) ou c est un construc-

teur de données structurées d'arité n, et p1 . . . pn sont des patterns euxmêmes.

iii. notion de dé�nitionUne dé�nition qui contient des patterns sur le coté gauche (avant =) est

dite : dé�nition qui utilise le pattern matching pour exécuter l'analyse descas .

On détaillera en ce qui suit ceci par des exemples :

Exemple :factorial 0 = 1factorial n = n * factorial (n-1)

Pour évaluer l'expression (factorial t) il y a deux choix à considérer, sit correspond à 0 alors la première équation est choisie, sinon la mise encorrespondance se fait avec n, ici il n'y a que 2 équations dont la deuxièmegarantie la mise en correspondance.

Lors de l'analyse syntaxique, un ensemble d'équations qui sont reconnuesfaire partie d'une même dé�nition qui utilise le pattern matching, grâce auxdeux caractéristiques suivantes :

� le premier nom qui apparaît dans une équation est commun, et repré-sente le nom de la fonction à dé�nir.

� Les équations d'une même dé�nition sont regroupées l'une après l'autre.

iv. Introduction aux types structurésa. Catégories des types structurés :

nous avons dé�nit dans le premier chapitre comment se fait la déclarationet l'utilisation des types structurés (dits aussi composés), maintenant nousallons détailler leur implémentation.

Les types structurés sont des types qui possèdent un ou plusieurs construc-teurs de types ayant les caractéristiques des fonctions. Ces fonctions quandelles sont appliqués à des arguments ne se réduisent pas (c.à.d en forme nor-male).

En général il y a deux catégories de types structurés :

Page 47: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 40

� type sommation (noté par sum-type), qui sont les types décrits par lanotations suivantes :

data T = C1 T1,1 . . . T1,r1 | C2 T2,1 . . . T2,r2| . . . | Cn Tn,1. . .Tn,rn

� type produit (notés par product-type), qui sont les uples comme lespaires, les triplets ... etc.

b. cas spéciauxCette section montre comment les listes, les uples, peuvent être regardés

comme des instances des types structurés généraux.Les listes qui ont une syntaxe spéciale dans les langages fonctionnels (sec-

tion 1.6), vu qu'elles sont beaucoup manipulées, mais elles ont les pro-priétés des types structurés, car on peu les dé�nir de la manière sui-vante :

data Liste a = Nil | Cons a (Liste a)

c'est à dire que :

� [] est translaté en Nil� (x:xs) est translaté en (Cons x xs)

Les uples comme les listes, ils ont une syntaxe spéciale mais ils gardent lesmême propriétés que les types structurés :

Exemple :

(a,b) peut être translatée en (Pair a b) ou Pair est un constructeur,d'où : ('a', (1,2) ) est translatée en (Pair Char (Pair Int Int)).

Donc nous pouvons enrichir le schéma de translation par ce que suit :

TE[

:]≡ Cons (3.10)

TE[

[ ]]≡ Nil (3.11)

TE[

[E1, E2, . . . , En]]≡ Cons TE

[[E1]

]TE[

[E2, . . . , En]]

(3.12)

Page 48: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 41

TE[

(E1, E2)]≡ Pair TE

[E1

]TE[E2

](3.13)

La facilité de translation des types prédé�nis comme les listes et lesuples simpli�e l'implémentation puisqu'au lieu d'implémenter plusieurs mé-canismes, pour les listes et pour les uples ; il su�t d'implémenter un seulmécanisme de types structurés et de translater les autres types structurés,vers ce type déjà implémenté.

v. Translation vers le lambda calcul enrichiPour expliquer comment se fait la translation, on a besoin d'introduire

deux caractéristiques du lambda calcul enrichi :� Les lambda abstractions qui utilisent le pattern matching (les patternmatching lambda abstractions).

� L'opération in�x prédé�ni [] (nommé fat bar).Une translation d'une dé�nition est donnée comme suit :

TD[f v = E

]≡ f = λv.TE

[E

]Or p est un pattern, c'est-à-dire il peut être une constante ou un construc-

teur avec des arguments, donc la dé�nition devient :

TD[f v = E

]≡ f = λ TE

[v

].TE

[E

](3.14)

Exemple :fst (x,y) = x

est transformée comme suit :

TD[fst (x, y) = x

]≡ f = λ (Pair x y).x

par cela on a introduit une nouvelle sorte de lambda abstractions (λp.E)avec p qui n'est pas seulement une variable.

a. Équations multiplesOn considère la dé�nition :

Page 49: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 42

f p1 = E1

f p2 = E2...f pn = En

intuitivement, on peut dire que la sémantique est : "essayer la premièreéquation si il y a échec, essayer la deuxième, et ainsi de suite", cela veutdire que la mise en correspondance peut échouer, donc nous introduisons unenouvelle valeur prédé�ni FAIL qui n'est pas équivalente à une erreur, tantqu'il y a seulement échec de mise en correspondance.

Avec cette idée on translate la dé�nition de f par :

f = λx.( ((λp1.E1 ) x)

[] ((λP2.E2) x)...[] ((λPn.En) x)

[] ERROR)

où x est une nouvelle variable, Ei est la translation de Ei, pi est la trans-lation de pi.

[] est un opérateur associatif prédé�ni, tel que :a [] b = a, si a 6= FAILFAIL [] b = b

C'est-à-dire si le premier argument de [] est FAIL alors on retourne ledeuxième.

Ainsi, cette nouvelle dé�nition se lit : "essayer d'appliquer (λp1.E1) à x ettrouver son résultat sinon continuer, jusqu'à (λpn.En), s'il y a échec, évaluerERROR qui est une fonction prédé�ni retournant un message d'erreur"

Exemple :La dé�nition de factorial se translate en :

factorial = λt.( ((λ0.1 ) t)

[] ((λn.(∗ n (factorial (− n 1)))) t)

[] ERROR)

dans ce cas l'erreur n'est jamais atteinte.

Page 50: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 43

b. Arguments multiplesune dé�nition de la forme : f p1 p2 . . . pn = E peut être aisément tra-

duite en :

f = λv1 . . . λvn.(((λp1 . . . λpn.E) v1 . . . vn) [] ERROR)

où v1 . . . vn sont des variables libres, pi et E sont respectivement la trans-lation de pi et E, avec considération de ce qui suit : quand l'évaluation avecle premier argument échoue le résultat devient :

(λp1 . . . λpn.E) E1 E2 . . . En → FAIL E2 . . . En

il faut que l'évaluation de toute l'expression échoue, donc on a besoind'une règle de réduction pour FAIL qui est :

FAIL E → FAIL

Pour dire que :FAIL E2 E3 . . . En → FAIL E3 . . . En → . . .→ FAIL

Exemple :and True b = bans False b = False

est translatée en :

and = λt1.λt2.( ((λTrue.λb.b ) t1 t2)

[] ((λFalse.λb.False ) t1 t2)

[] ERROR)

c. Équations conditionnellesprenons par exemple le fonction signe dé�nie par :

signe a = 1, if (a>0)= 0, if (a==0)= -1, if (a<0)

On peut facilement translater le coté droit de la dé�nition en lambdacalcul enrichi, ce qui donne :

Page 51: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 44

signe = λa.(IF (> a 0) 1

(IF (= a 0) 0

(IF (< a 0) (neg 1) FAIL)))

Où neg est une fonction prédé�nie qui retourne la valeur négative de sonargument. Notons que si toutes les conditions ne se véri�ent pas, alors FAILest retourné indiquant l'échec. En précisant que le coté droit de la dé�nitioncommence a partir du premier '=', il est possible de donner un nouveauschéma qui traduit le coté droit des dé�nitions (TR) :

TR

A1, if G1

= A2, if G2...

= An, if Gn

≡(IF TE

[G1]TE[A1]

(IF TE[G2]TE[A2]

...(IF TE

[Gn

]TE[An]FAIL) . . . ))

(3.15)

On utilise TR à la place de TE pour la translation du côté droit desdé�nitions.

On peut trouver des dé�nitions qui utilisent le pattern matching et leséquations conditionnelles.

Exemple :factorial 0 = 1factorial n = n * factorial ( n - 1 ), if ( n > 0 )

devient :

factorial = λt.( ((λ0.1 ) t)

[] ((λn.(IF (> n 0) (∗ n (factorial (− n 1))) FAIL)) t)

[] ERROR)

d. Les dé�nitions locales avec "where"Prenons un exemple :f x y = a * b

wherea = x + yb = x - y

Page 52: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 45

intuitivement elle se translate clairement en :

f = λx.λy.( let a = + x y

b = − x y

in (∗ a b))

En général les dé�nitions incluses après where sont mutuellement récur-sives. Donc on devrait utiliser letrec (section 2.3.3), on verra par la suitequelque optimisations qui permettent de choisir entre let et letrec.

Donc le schéma TR devient :

TR

A1, if G1

= A2, if G2...

= An, if Gn

whereD1...Dn

letrec TD[D1]

...TD

[Dn

]in (IF TE

[G1]TE[A1]

(IF TE[G2]TE[A2]

...(IF TE

[Gn

]TE[An]FAIL) . . . ))

(3.16)

e. ConclusionLe schéma TD �nal devient :

TD

f p1,1 . . . p1,m = R1

. . . . . . . . .f pn,1 . . . pn,m = Rn

f = λv1 . . . λvm.( ((λ TE[p1,1] . . . λ TE[p1,m].TR[R1]) v1 . . . vm)

[] . . . . . . . . .

[] ((λ TE[pn,1] . . . λ TE[pn,m].TR[Rn]) v1 . . . vm)

[] ERROR)(3.17)

où :

f est une variable, vi est une variable, pi,j est un pattern.

Page 53: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 46

3.2.4 Translation des listes de compréhensioni. Introduction

Comme le pattern matching, les listes de compréhension (section 1.6.4)sont une extension syntaxique pour les programmes fonctionnels puisqueces derniers peuvent être facilement transformées en des programmes quine contiennent pas de listes de compréhension.

On considère l'exemple mathématique suivant sur la dé�nition d'un en-semble :

B = { square x / x ∈ A et odd x }

Cette dé�nition correspond à la dé�nition suivante en utilisant les listesde compréhension :

ys = [ square x | x <- xs, odd x]La forme générale des listes de compréhension est, telle que dé�nie dans

le premier chapitre :[ < expression > | < qualifier >, ... , < qualifier > ]

où < qualifier > est soit un �ltre ( odd x ) ou un générateur ( commex <- xs )

Exemple :set = [ x | x <- [ 3, 1, 5, 4, 0 ], x > 2 ]

donne :[ 3, 5, 4 ]

ii. Translation en lambda calcul enrichiC'est une extension du schéma de translationTE, il faut ajouter une fonc-

tion prédé�nie flatMap ayant deux arguments où l'évaluation de ( flatMap f xs) applique f à chaque élément de la liste xs et concatène les résultats.

Exemple :flatMap square [ 1, 2, 3 ] = [ 1, 4, 9 ]

TE[[E | v ← L, Q]

]≡ flatMap (λv.TE

[[ E ] | Q]

]) TE

[L

](3.18)

Page 54: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 47

TE[[E | B , Q]

]≡ IF TE

[B

]TE[[ E ] | Q]

]NIL (3.19)

TE[[E | ]

]≡ Cons TE

[E

]NIL (3.20)

Où :� E est une expression� L est une liste� B est une expression booléenne.� Q est une séquence de 0 ou plusieurs quali�cateurs� v est une variable

Exemples :

TE[[square x | x ← xs, odd x]

]= flatMap (λx.TE

[[square x ] | odd x]

]) xs

(règle 3.18)

= flatMap (λx.(IF (odd x) TE[[square x | ]

]NIL)) xs

(règle 3.19)= flatMap (λx.(IF (odd x) (Cons (square x) NIL) NIL)) xs

(règle 3.20)un autre exemple :

TE[[ (x, y) | x ← xs, y ← ys]

]= flatMap (λx.TE

[[ (x, y) ] | y ← ys]

]) xs

(règle 3.18)

= flatMap (λx.(flatMap (λy.TE[[ (x, y) ] | ]

]) ys) xs

(règle 3.18)= flatMap (λx.(flatMap (λy.(Cons (Pair x y) NIL)) ys) xs

(règle 3.20)

Page 55: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 48

3.3 Transformation du lambda calcul enrichi enlambda calcul ordinaire

L'introduction du lambda calcul enrichi avec ces nouvelles constructionscomme une étape intermédiaire, simpli�e l'implémentation, surtout lorsqu'ils'agit du pattern matching avec l'utilisation des lambda abstractions incluantle pattern matching et l'opérateur fat bar ([]).

Ce qu'on montrera ici est comment transformer les expressions en lambdacalcul enrichi vers une syntaxe plus simple qui est celle du lambda calculsimple.

3.3.1 Transformation des lambda abstractions incluantle pattern matching

En lambda calcul enrichi les lambda abstractions sont écrites de la forme(λp.E) où p est un pattern, c-à-d que p est soit une simple variable, soit uneconstante, ou bien un constructeur de donnée.

Dans le cas où p est une variable la lambda abstraction est en lambdacalcul simple, donc pas de modi�cations à faire, les autres cas sont traitésdans ce qui suit.

a. Patterns constantsEn sémantique opérationnelle une lambda abstraction de la forme λk.E

(où k est une constante) appliquée à un argument a, est évaluée en faisantun test si a est égale à k, si tel est le cas le résultat retourné est E, sinon,c'est un échec d'évaluation.

Cela nous conduit à la transformation suivante :

(λk.E) ≡ (λv.IF (= k v) E FAIL)

où v est une variable qui ne possède pas d'occurrence libre dans E.Notons que les deux expressions sont équivalentes et on peut le démontrer

sémantiquement.Pour véri�er prenons un exemple :

f 0 = 1f 1 = 0

qui va être translaté en :

Page 56: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 49

f = λx.( ((λ0.1) x)

[] ((λ1.0) x)

[] ERROR)

puis vers le lambda calcul simple :

f = λx.( ((λv.IF (= 0 v) 1 FAIL) x)

[] ((λv.IF (= 1 v) 0 FAIL) x)

[] ERROR)

On pourrait véri�er simplement que :

f 0 → . . . → 1

f 1 → . . . → 0

f 2 → . . . → ERROR

b. Patterns constructeurs de type produitConsidérons l'expression (λp.E) où p est un pattern représenté sous la

forme (t p1 p2 . . . pr), où t est un constructeur de type produit d'arité r.Si on introduit une fonction primitive qui nous permet d'accéder au com-

posants p1 p2 . . . pr , c-à-d un sélecteur SEL 1 où

SEL-t-i = pi

Avec i l'index du composant à extraire et t le type en question, alors lalambda expression :

(λ(t p1 p2 . . . pr).E) a

serait sémantiquement équivalente à :

(λp1 .λp2 . . . λpr.E) (SEL-t-1 a) . . . (SEL-t-r a)

1SEL est une primitive tel que + ou encore IF .

Page 57: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 50

Exemple :

(λ(Pair x y).E) (Pair 3 4)

≡ (λx.λy.E) (SEL-Pair-1 (Pair 3 4)) (SEL-Pair-2 (Pair 3 4))

≡ (λx.λy.E) 3 4

Cela nous conduit à la transformation :

((t p1 . . . pr).E) ≡ UNPACK-PRODUCT-t (λp1 . . . pr.E)

Où UNPACK-PRODUCT est une fonction primitive qui prends deuxarguments, une fonction et un objet structuré, et qui applique la fonctionaux champs sélectionnés de cet objet. Elle est dé�nie sémantiquement par :

UNPACK-PRODUCT-t f a = f (SEL-t-1 a) . . . (SEL-t-r a)

Après la transformation on remarque que l'expression (λp1. . . . λpr.E) ré-sultante peut toujours être en lambda calcul enrichi, car p1, . . . , pr sonttoujours des patterns, alors il faut réappliquer la transformation pour élimi-ner ces patterns, jusqu'à ce qu'il ne reste plus de lambda abstraction utilisantle pattern matching.

Exemple :add = λ(Pair x y).+ x y

qui est transformée en :

add = UNPACK-PRODUCT-Pair (λx.λy.+ x y)

On peut véri�er qu'elle donne le même résultat en réduisant (add (Pair 3 4)).

add (Pair 3 4) = UNPACK-PRODUCT-Pair (λx.λy.+ x y) (Pair 3 4)

→ (λx.λy.+ x y) (SEL-Pair-1 (Pair 3 4)) (SEL-Pair-2 (Pair 3 4))

→ + (SEL-Pair-1 (Pair 3 4)) (SEL-Pair-2 (Pair 3 4))

→ + 3 4

→ 7

Page 58: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 51

c. Patterns constructeurs de type sommationConsidérons maintenant l'expression (λp.E) où p est un pattern repré-

senté sous la forme (sp1 . . . pr), ce cas est di�érent du précédent parce que untype sommation peut posséder plusieurs constructeurs, donc avant d'appli-quer une quelconque transformation il faut tester si l'argument de la lambdaabstraction possède le même constructeur que le pattern.

Exemple :

(λ(Cons x xs).E) (Cons 1 NIL)→ (λx.λxs.E) 1 NIL

(λ(Cons x xs).E) NIL→ FAIL (echec)

Pour cela on introduit une nouvelle fonction primitive, UNPACK-SUM-soù

(λ(s p1 . . . pr).E) ≡ UNPACK-SUM-s (p1 . . . pr.E)

où UNPACK-SUM-s prend deux arguments, une fonction et un objetstructuré, et véri�e si l'objet est construit avec le même constructeur s, sitel est le cas UNPACK-SUM-s retourne une application de la fonction auxcomposants de l'objet, sinon c'est un échec d'évaluation.

UNPACK-SUM-s f (s a1 . . . ar) = f a1 . . . ar

UNPACK-SUM-s f (s a1 . . . ar) = FAIL, if s 6= s

Comme exemple prenons la dé�nition :last (x : []) = xlast (x : xs) = last xs

elle est translatée en :

last = λt.( ((λ(Cons x NIL).x) t)

[] ((λ(Cons x xs).last xs) t)

[] ERROR)

en appliquant la transformation, il en résulte :

last = λt.( (UNPACK-SUM-Cons (λx.λNIL.x) t)

[] (UNPACK-SUM-Cons (λx.λxs.last xs) t)

[] ERROR)

Page 59: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 52

d. ConclusionVoici l'ensemble des transformations développées pour les lambda abs-

tractions utilisant le pattern matching.

(λk.E) ≡ (λv.IF (= k v) E FAIL) (3.21)

où v est une variable qui n'a pas d'occurrences libres dans E

(λ(t p1 . . . prt).E) ≡ (UNPACK-PRODUCT-t (p1 . . . prt .E)) (3.22)

(λ(s p1 . . . prs).E) ≡ (UNPACK-SUM-s (p1 . . . prs .E)) (3.23)

où k est une constante, t est un constructeur de type produit d'arité rtet s est un constructeur de type sommation d'arité rs

avec :

UNPACK-PRODUCT-t f a = f (SEL-t-1 a) . . . (SEL-t-r a)

UNPACK-SUM-s f (s a1 . . . ar) = f a1 . . . ar

UNPACK-SUM-s f (s a1 . . . ar) = FAIL, if s 6= s

3.3.2 Transformation des let et letrecDans ce langage les dé�nitions locales sont représentées par les blocs

where et let...in, dans lesquels ces dé�nitions sont en générale mutuellementrécursives, alors une transformation de ces blocs en letrec (section 2.3.3)pourrait su�re, mais il arrive des cas où les dé�nitions ne sont pas récursiveset donc, les transformer en let (section 2.3.3) serai plus optimal. Il arriveaussi des cas où la récursivité mutuelle ne concerne pas toutes les dé�nitionsdans le bloc alors les dé�nitions sont triées en des petits groupes minimauxgrâce à ce qu'on appel l'analyse des dépendances.

i. Analyse des DépendancesConsidérons l'expression suivante :

Page 60: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 53

letrec x = fac z

fac = λn.IF (= n 0) 1 (+ n (fac (− n 1)))

z = 4

sum = λx.λy.IF (= x 0) y (sum (− x 1) (+ y 1))

in sum x z

une expression équivalente et plus clair est :

let z = 4

in letrec

sum = λx.λy.IF (= x 0) y (sum (− x 1) (+ y 1))

in letrec

fac = λn.IF (= n 0) 1 (+ n (fac (− n 1)))

in let

x = fac z

in sum x z

Cette expression expose clairement quelle dé�nition dépend de quelleautre dé�nition, et l'utilisation des letrec n'est réservée qu'aux dé�nitionsdans lesquelles elle est nécessaire, même quand la récursivité est utilisée, lesgroupes de dé�nitions mutuellement récursives sont séparés dans des groupesde letrec. Cette transformation est appelée analyse des dépendances.

L'analyse des dépendances est utile parce que les expressions let ont unrendement plus e�cace que les letrec.

On décrira avec plus de détail l'analyse des dépendances en utilisantl'exemple suivant :

letrec a = . . .

b = . . . a . . .

c = . . . h . . . b . . . d

d = . . . c . . .

f = . . . g . . . h . . . a . . .

g = . . . f . . .

h = . . . g . . .

in . . .

Page 61: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 54

a

b

c d

f g

h

?

?

�� @@- -

?-

- � �?

��

����

Fig. 3.2 � Exemple de graphe de dépendance

L'algorithme de transformation est divisé en 4 étapes :1. Pour chaque expression letrec, construire un graphe où chaque noeud

représente une variable locale, il y'a un arc de f à g si g occure librementdans f (�gure 3.2).

2. Deux variables (x, y) sont mutuellement récursives s'il y'a un cheminde x à y et de y à x, en théorie des graphes cela nous rappelle lescomposantes fortement connexes, l'exemple précédent devient

{c, d}, {f, g, h}, {b}, {a}

3. On tri les composantes fortement connexes dans un ordre de dépen-dance (tri topologique, �gure 3.3)Le résultat du tri topologique devient : {c, d}, {b}, {f, g, h}, {a}.

4. Finalement, on génère des expressions let et letrec pour chaque groupetopologiquement trié.

Page 62: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 55

{b}

{a,d}

{f,g,h}

{a}-

? ?

Fig. 3.3 � Composantes connexes

let a = . . .

in let

b = . . . a . . .

in letrec

f = . . . g . . . h . . . a . . .

g = . . . f . . .

h = . . . g . . .

in letrec

c = . . . h . . . b . . . d

d = . . . c . . .

in . . .

ii. Transformation� Une simple let expression peut être aisément translatée suivant le schéma

de translation suivant :

(let v = B in E) ≡ ((λv.E) B)

� Une let expression contenant plusieurs dé�nitions :

Page 63: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 56

let v1 = B1

v2 = B2

...vn = Bn

in E

≡ (λv1 .λv2 . . . λvn.E) B1 B2 . . . Bn

� Une simple letrec expression peut être transformée en une let expressionen enlevant la récursivité par l'opérateur récursif Y (section 2.2.5) :

(letrec p = B in E) ≡ (let p = Y (λp.B) in E)

� Dans une letrec expression contenant plusieurs dé�nitions, se pose unproblème, en e�et quand il y'a des dé�nitions mutuelles récursives, parexemple :

letrec x = Cons 1 y

y = Cons 2 x

in E

Une solution consiste à regrouper tout les cotés droits des dé�nitionsdans un uple et lier chaque variable locale à la composante qui corres-pond dans le uple, c-à-d :

letrec v = (Pair (Cons 1 (SEL-Pair-2 v)) (Cons 2 (SEL-Pair-1 v)))

in let x = SEL-Pair-1 vy = SEL-Pair-2 v

in E

donc le schéma devient :

letrec v1 = B1

v2 = B2

...vn = Bn

in E

letrec v = (t B1 . . . Bn)

in let v1 = SEL-t-1 v...vn = SEL-t-n v

in E

Page 64: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 57

où B1 . . . Bn sont respectivement les résultats des substitutions de v1, . . . , vnpar (SEL-t-1 v) . . . (SEL-t-n v) dans B1 . . . Bn.

3.3.3 Transformation des case expressionsUne expression case est de la forme :

case v of

p1 → E1

p2 → E2

...pn → En

Où p1 . . . pn sont des patterns, v est une variable. Ei des expressions.Selon pi, on distingue trois cas :

i. ConstantesOn peut aisément faire cette translation suivant le schéma suivant :

case v of

k1 → E1

k2 → E2

...kn → En

(IF (= v k1) E1

(IF (= v k2) E2

...(IF (= v kn) En FAIL) . . . )

(3.24)

ii. Type Produitc-à-d que l'expression est de la forme :

case v of

t v1 . . . vr → E

Ce cas est facile à traiter, parce qu'il y'a toujours un seul choix à faire :

case v of

t v1 . . . vr → E≡ UNPACK-PRODUCT-t (λv1 . . . vr.E) v

(3.25)

Page 65: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 58

Prenons par exemple la dé�nition :

addPAIR = λw.(case w of (Pair x y) → (+ x y))

elle devient :

addPAIR = λw.(UNPACK-PRODUCT-Pair (λx.λy.+ x y) w)

avec une η réduction (section 2.2.5) on trouve :

addPAIR = (UNPACK-PRODUCT-Pair (λx.λy.+ x y))

iii. Type SommationSupposons que les constructeurs sont des constructeurs de type somma-

tion :

case v of

(s1 v1,1 . . . v1,r1)→ E1

...(sn vn,1 . . . vn,rn)→ En

On peut la transformer en utilisant le schéma suivant :

case v of

(s1 v1,1 . . . v1,r1)→ E1

...(sn vn,1 . . . vn,rn)→ En

CASE-T v (UNPACK-SUM-s1 (λv1,1 . . . v1,r1 .E1) v)...

(UNPACK-SUM-sn (λvn,1 . . . vn,rn .En) v)

(3.26)

avec la fonction primitive CASE-T qui pour chaque type T choisi un deces n argument en dépendant du constructeur utilisé pour construire le pre-mier argument :

CASE-T (si a1 . . . ari) b1 . . . bi . . . bn = bi

Page 66: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 59

3.3.4 L'Opérateur []L' opérateur [] est transformé en une fonction primitive nommée FATBAR

où :

FATBAR a b = a, if a 6= FAILFATBAR FAIL b = b

3.3.5 ConclusionDans cette partie on a traité la transformation du lambda calcul enrichi

en lambda calcul simple, en utilisant l'opérateur Y pour exprimer la récur-sivité, mais dans les prochaines parties on montrera que la transformationdes let, letrec et même l'utilisation de l'opérateur Y ne sont pas forcémentnécessaires puisqu'on peut directement générer du code compilé à partir deslets et letrecs, et on peut même exprimer la récursion en transformant lesarbres des expressions en graphes.

3.4 Techniques d'évaluation3.4.1 Introduction

A partir de cette section, on montrera comment le lambda calcul ordi-naire peut être implémenté en utilisant une méthode appelée réduction degraphes. Cette méthode consiste à donner une autre représentation plusconcrète aux lambda expressions, et ceci pour faciliter l'implémentation del'évaluateur, en général cette technique possède les caractéristiques suivantes :

1. L'exécution d'un programme fonctionnel consiste en l'évaluation d'uneexpression.

2. Un programme fonctionnel est représenté sous forme d'arbre ( plusgénéralement un graphe ).

3. L'évaluation est alors une succession d'étapes appelées réductions, chaqueréduction fait une transformation locale du graphe (d'où le terme ré-duction de graphes).

4. Les réductions peuvent être dans di�érents ordres, ou même en parallèlesi, bien sûr, il n'y a pas de dépendance entre les réductions en question.

5. L'évaluation est complète s'il n'y a pas de sous expressions qui nécessiteune réduction. L'expression est dite alors sous forme normale.

Page 67: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 60

Cette technique donne un autre modèle sur les machines d'exécutions,qui est complètement di�èrent du modèle classique (utilisé pour les langagesconventionnels). On parlera donc de la représentation concrète des lambda ex-pressions, ensuite nous allons décrire la méthode d'évaluation sur les graphes(basée sur les α, β, η réductions section 2.2.5).

On passera ensuite à une technique plus e�cace qui utilise la notiondes supercombinateurs, et en�n on décrira deux techniques assez récentesappelées machine SK et G-machine.

3.4.2 Environnement à l'évaluationOn donnera l'environnement à l'évaluation suivant une représentation

concrète des lambda expressions (stockage en mémoire), la gestion de la mé-moire, les notion liées à la réduction de graphes et le mécanisme de réduction.

i. Représentation des programmesOn donnera une représentation concrète des lambda expressions, c'est-à-

dire comment elle seront stockées en mémoire [Pey87].

a. Arbres syntaxiques abstraitsDans toute implémentation de réduction de graphes, l'expression est re-

présentée par un arbre syntaxique abstrait, dont les feuilles sont des constantestel que 0, 1, 'a', ou des fonctions prédé�nis (+, -, *, cos) ou des variables.

L'application d'une fonction f à un argument x est représentée par :

@/ \f x

Exemples :l'expression (+ 1 2) qui veut dire ,par curry�cation, la fonction (+) qui

est appliquée à 1 et qui retourne une fonction (+ 1) appliquée à l'argument2 :

(+ 1 2) -----> @/ \

(+ 1) ---> @ 2/ \

(+) -> + 1

Page 68: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 61

un autre exemple :

@/ \/ \@ @/ \ / \+ 2 @ 5

/ \* 3

qui veut dire (+ 2 (∗ 3 5))

Une lambda abstraction (λx.corps) est représentée par :

\x|

corps

Une expression tel que (Cons E1 E2) est représentée par :

@/ \@ E2/ \

Cons E1

Cons est considéré comme une fonction prédé�nie recevant deux argu-ments et qui donne une expression qui est en forme normale (c-à-d qui ne seréduit pas), on l'appelle alors fonction constructeur de type ((a → liste) →liste).

Aussi, une expression telle que (E1, E2) qui est représentée en lambda cal-cul par (Pair E1 E2) où pair est une fonction constructeur, et une expressionen forme normale.

Page 69: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 62

@/ \@ E2/ \

Pair E1

Le processus de réduction se réalise en une succession de transformationssur l'arbre, durant ce processus l'arbre devient un graphe (section 3.4.2).

b. Représentation concrète du grapheL'image qui a été montrée est abstraite, dans l'implémentation chaque

noeud du graphe est stocké dans un espace contigu qu'on appelle cellule.Une cellule contient :

� Une information concernant le type du noeud (application, nombre,caractère, lambda abstraction, variable, fonction prédé�nie ...).

� Deux champs au minimum qui servent comme des pointeurs vers d'autrescellules ou tout simplement contenir des données (qui ne sont pas despointeurs).

ii. Gestion de la mémoireLe processus de réduction implique l'allocation de nouvelles cellules pour

la construction de nouvelles parties du graphe. Aussi le processus de réductionimplique la libération de cellules allouées et non utilisables ; on peut savoirqu'une cellule est utilisable si c'est une cellule qui n'est pas pointées pard'autres cellules (c.à.d qui n'appartient pas au graphe), dans ce cas il estnécessaire de réutiliser ces cellules car autrement on épuisera l'ensemble descellules libres si on ne les mets pas à jour régulièrement.

Ce mécanisme s'appelle ramassage des miettes (garbage collection).Toute implémentation de langage fonctionnels nécessite un ramassage de

miettes pour identi�er les cellules inutilisées a�n de les recycler. Le mécanismed'allocation de cellules et de ramassage des cellules inutilisés est connu sousle nom de gestion automatique de la mémoire.

iii. Réduction de graphesQuand un programme fonctionnel est chargé en mémoire, l'évaluateur est

appelé à réduire le graphe en forme normale en faisant une succession deréductions sur le graphe, suivant deux taches essentielles et di�érentes :

� Sélection du prochain redex (section 2.1).� Réduction du redex sélectionné.

Page 70: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 63

Comme il a été dit plutôt (chapitre 2), l'ordre de réduction est importantpuisqu'il a�ecte la nature du résultat du programme. L'ordre de réductionest basé sur les notions suivantes :

a. Évaluation souple (paresseuse)Dans les langages impératifs (tel que Pascal ou C ), les arguments de la

fonction sont évalués avant que la fonction soit exécutée (appel par valeur),cependant il est possible que ces arguments ne soit pas utilisés dans le corpsde la fonction, ce qui implique l'existence d'un travail sans intérêt, ceci pousseà suggérer de retarder l'évaluation des arguments jusqu'au besoin (appel parbesoin ou par nécessité).

L'appel par nécessité est rarement implémenté dans les langages impé-ratifs car le retardement de l'évaluation des arguments peut engendrer unrésultat di�èrent. Dans les langages fonctionnels purs, une évaluation de lamême expression donne le même résultat quel que soit l'appel ce qui éviteun tas de problème, sauf qu'il y a un inconvénient pour l'évaluation souple :le temps d'exécution, mais il existe des langages qui utilisent les deux méca-nismes d'évaluation, par valeur et souple (ML, Hope).

Chaque implémentation d'évaluation souple possède deux notions :

� Les arguments de la fonction sont évalués uniquement par besoin.� Si un argument est évalué il ne sera pas évalué dans une autre occur-

rence de l'argument puisque le résultat ne change pas dans les di�é-rentes circonstances

L'argument est donc évalué au maximum une seule fois si la fonction ena besoin.

La première notion est réalisée en implémentant directement l'ordre nor-mal de réduction qui consiste à évaluer le redex le plus à gauche, c'est à direen donnant une application f x, f est réduite avant x. La deuxième notionfait intervenir la réduction elle-même et sera donc traité plutard (section3.4.2).

b. Mécanismes d'entrée/sortie (E/S)Il existe des cas où le résultat d'un programme est une liste in�nie, l'éva-

luation complète puis l'écriture pose des problèmes (ici l'évaluation ne ter-mine jamais), alors une solution est d'écrire ces éléments de la liste dès leursgénération.

Les Entrée/Sortie sont considérées comme des e�ets de bord dans leslangages impératifs. Puisque les langages fonctionnels n'acceptent pas lese�ets de bord, alors les programmes sont considérés comme des fonctions qui

Page 71: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 64

pgm fonctionnel- -Entrée(liste in�nie)

Sortie(liste in�nie)

Fig. 3.4 �

reçoivent des données d'entrée et produisent des données de sortie (�gure3.4) :

Données d'entréeLes données en entrée sont considérées comme une liste in�nie, le pro-

gramme a besoin d'évaluer la liste élément par élément, alors il est importantque la première évaluation ne traite pas la liste directement, mais dès qu'unélément est entré il est utilisé directement.

Données de sortieL'écriture des structures de données se fait dès leurs générations, donc on

peut dire que l'évaluateur est appelé par la fonction d'écriture. Si le résultatde l'évaluation est un nombre (caractère, booléen,...) l'évaluation est com-plète et le résultat est écrite directement, sinon, le résultat serait forcémentun constructeur (tel que Cons) alors ces composants (head, tail) sont évaluéssuccessivement l'un après l'autre, et sont écrits dès leur évaluation.

Exemple :En admettant qu'il n'y a que le type nombre et type liste de nombres

l'algorithme est le suivant :

Ecrire(E){E' := Evaluer(E)Si (estNombre(E')) alors Sortie(E')Sinon {

Ecrire(Head(E'))Ecrire(Tail(E'))}

}

Les constructeurs dans ce cas sont appelés constructeurs souples (pares-seux) puisqu'on n'évalue pas directement leurs composants, mais en fonction

Page 72: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 65

du besoin. Tout cela n'est qu'une extension du caractère souple (paresseux)de ces langages.

c. Formes WHNFD'après ce qui a été exposé, on peut dire que :

L'évaluation d'une expression qui conduit à un constructeur (Cons) nese poursuit pas jusqu'à l'évaluation du Head et Tail, ce qui veut dire qu'onarrête la réduction alors qu'il pourrait encore exister des redex dans l'expres-sion, dans ce cas l'expression est dite sous forme WHNF (Weak Head NormalForm).

Formellement, une lambda expression est en WHNF si est seulement sielle est de la forme :

f E1 E2 . . . En

où :n ≥ 0 et f est variable ou donnée et (f E1 E2 . . . Em) n'est pas un redex

pour m ≤ n

Donc l'évaluateur n'évalue pas jusqu'à la forme normale mais il s'arrêtedès que l'expression est en WHNF.

iii. Mécanisme de réductionAinsi qu'il a été vu plus haut (section 3.4.2), la réduction de graphes est

basée sur le mécanisme suivant formé des deux étapes :

� Sélection du prochain redex.� Réduction du redex sélectionné.

a. Sélection du prochain redexL'expression à évaluer est toujours de la forme f E1 E2 . . . En c'est à

dire :

Page 73: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 66

@/ \. En..@/ \@ E2/ \f E1

On doit respecter l'ordre normal de réduction en réduisant uniquementles redex les plus a gauche du niveau supérieur de l'expression (section 2.2.5).

Il y a plusieurs possibilités :

� f est une donnée (nombre, constructeur) alors l'expression est enWHNF.� f est une fonction primitive (prédé�nie) d'arité k alors si n = k le redex

est (f E1 E2 . . . Ek) sinon l'expression est en WHNF.� f est une lambda abstraction de la forme (λx.corps) alors le redex est

(f E1) si n = 1, ou alors l'expression est en WHNF.Le redex est marqué par $, comme montré dans les deux arbres suivants :

@ @/ \ / \

$@ E3 $@ E2/ \ / \@ E2 \x E1/ \ |+ E1 corps

Pour atteindre la cellule de f il faut parcourir le graphe sur le côté gauchejusqu'à ce que le prochain noeud ne soit pas une application, mais il faut aussigarder la trace des arguments E1 . . . En qui serviront pour la réduction, doncon suggère de mettre une pile de pointeurs vers la liste des noeuds commeillustré dan la �gure suivante :

Page 74: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 67

______base | ___|_____________ @

|______| / \| . | . En| . | .|______| .| ___|________ @|______| / \| ___|_______f E1

sommet |______|

De cette façon les arguments sont tous accessibles à travers la pile et leurnombre est tiré de la taille de la pile.

Une fonction primitive telle que (+) est une fonction stricte c'est à direqui évalue ses arguments pour donner leur addition ; dans ce cas l'évaluateurs'invoque lui même (récursivement) pour évaluer les arguments de la fonctionavant son évaluation, et ceci en utilisant la même pile puisque l'évaluationde l'argument ne change pas les éléments se trouvant déjà dans la pile.

b. Réduction du redex sélectionnéSachant comment trouver le prochain redex, on passe directement à la

réduction, qui est le coeur de l'implémentation. On rappelle qu'une réductionéquivaut à une transformation locale du graphe qui représente l'expression àévaluer.

Après la sélection du prochain redex à évaluer, l'état de la pile associéeà l'évaluateur se trouve avec une fonction pointée par le sommet de la pile(�gure précédente).

Cette fonction est soit une lambda abstraction soit une fonction primitive,on décrira les deux cas séparément.

Réduction d'une application de lambda abstractionSi le sommet de la pile pointe vers une lambda abstraction, le redex

sélectionné est une application de la lambda abstraction à un argument, laréduction consiste alors à instancier le corps de la lambda abstraction avecla substitution du paramètre formel par l'argument, c'est à dire applicationd'une β réduction (section 2.2.5).

Exemple :

(λx.not x) True β−→ not True

Page 75: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 68

@ @/ \ / \

\x True not True| --->@/ \

not x

Trois problèmes surgissent alors :� L'argument pourrait contenir des redex, s'il y a plusieurs occurrences

du paramètre formel dans le corps alors toutes ces occurrences serontremplacées par une copie de l'argument, et dans ce cas l'évaluation del'argument peut être répétée.La meilleur façon d'éviter cela est de substituer le paramètre formeldans le lambda corps par un pointeur vers l'argument et de cette ma-nière si l'argument est réduit alors il le sera dans toutes ses occurrencesdans le lambda corps, ce qui implémente la deuxième notion de l'éva-luation souple.

Exemple :(λx.(∗ x x)) (+ 1 2)

@ @_ / \_ / \/ \ @ \

\x @ ---> / \___@| / \ * / \@ @ 2 @ 2/ \ / \ / \@ x + 1 + 1/ \* x

et ainsi l'arbre devient un graphe.� Le redex peut être partagé (référencé) par plusieurs expressions (sous

graphes), alors pour que le résultat de son évaluation soit lui aussipartagé il faut que la racine du redex soit écrasé physiquement par lerésultat.

Page 76: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 69

Exemple :Si on reprend l'exemple précédant (le $ marque le redex à évaluer)

@ $@ $9/ \ / \@ \ @ \ @/ \__$@ ---> / \___3 ---> 3* / \ * *

@ 2 @ 2 @ 2/ \+ 1 + 1 + 1

Notons que les fragments du redex ( ici *, +, @, 3, 2, 1) sont complète-ment détachés du graphe, il ne sont pas recouverts directement par leramasseur de miette parce qu'ils peuvent être référencés par d'autresparties, et donc utilisables.

� La lambda abstraction peut être appliquée plusieurs fois, donc la sub-stitution ne doit pas se faire directement sur le corps original de lalambda abstraction, on doit créer une copie du corps avec le paramètreformel substitué, en gardant un modèle de la lambda abstraction pourles prochaines utilisations.

Exemple :

$@ $@/ \ / \

\x True \x not True| ---> |@ @/ \ / \

not x not x

Voici le programme d'instanciation des lambda abstractions en C :

Page 77: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 70

Instancier (exp* corps, exp* var, exp* valeur){/* corps est un noeud d'application */

if (IsAp(corps))return(MakeAp(Instancier(GetFun(corps), var, valeur),

Instancier(GetArg(corps), var, valeur)));

/* corps est la variable var */if (IsVar(corps)){

if (corps == var) return (valeur);else return (corps);

}

/* corps est une lambda abstraction */if (IsLambda(corps)){

/* même paramètre formel */if (GetVar(corps)== var ) return(corps);elsereturn((MakeLambda(GetVar (corps),

Instancier(GetBody(corps), var, valeur)));}

/* corps est une constanteou une fonction primitive */return(corps);}

où :IsAp(B) vérifie si B est un noeud d'applicationGetFun(B) retourne la fonction a partir d'un

noeud d'applicationGetArg(B) retourne la l'argument a partir

d'un noeud d'applicationMakeAp(F,A) crée un noeud d'applicationIsVar(B) vérifie si B est un noeud de variableIsLambda(B) vérifie si B est un noeud de lambda abstractionGetVar(B) retourne le paramètre formel

d'un noeud d'abstractionGetBody(B) retourne le corps d'un noeud d'abstractionMakeLambda(V,B) crée un noeud de lambda abstraction

Page 78: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 71

Réduction d'une application de fonction primitiveDans ce cas, le redex à évaluer est une application de fonction primitive

à un certain nombre d'arguments. Si la fonction primitive a besoin d'évaluerces arguments avant de produire le résultat (les fonctions primitives sontgénéralement strictes), l'évaluateur est invoqué récursivement pour produirele résultat des arguments, par exemple pour la primitive (+) :

$@ $@/ \ / \@ 3 @ 3/ \ / \+ @ -----> + 3 -----> 6

/ \@ 2/ \+ 1

Dans certains cas il est nécessaire d'introduire un nouveau type de noeudsdit noeud d'indirection.

Un exemple su�t pour expliquer cette notion :

@ V/ \ |

\x False True| --->

True

V : noeud d'indirection

Pour produire une évaluation souple, la racine du redex (marquée par un$) doit être écrasée par le résultat. Dans cet exemple le lambda corps (qui estTrue) peut être copié directement dans la racine du redex. Mais, si le lambdacorps est une expression de taille plus importante alors il su�t de remplacerla racine du redex par un noeud d'indirection vers le lambda corps.

c. Algorithme �nal de réductionFinalement, les ingrédients de l'évaluation souple sont :1. ordre normal d'évaluation jusqu'en WHNF2. substitution par pointeurs

Page 79: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 72

3. écrasement de la racine du redex par le résultat

L'algorithme �nal de réduction est :

répéter1. parcours vers le coté gauche des noeuds d'applications en em-

pilant ces noeuds dans la pile, jusqu'à ce qu'on trouve unnoeud qui n'est pas une application.

2. si le noeud est :

une donnée quelconque : s'il n'y a pas d'arguments alorsl'expression est en WHNF et l'évaluation est terminée.

une fonction primitive : voir le nombre d'arguments dansla pile : s'il est inférieur à l'arité de la fonction, alorsl'expression est en WHNF, sinon, évaluer tout les argu-ments, exécuter la fonction primitive, et mettre à jour laracine du redex avec le résultat.

une lambda abstraction : si il n'y a pas un argument dis-ponible dans la pile alors l'expression est en WHNF,l'évaluation est terminée, sinon, substituer dans lelambda corps, le paramètre formel par des pointeurs versl'argument et mettre à jour la racine du redex.

�n

3.5 Supercombinateurs et lambda liftingEn se basant sur le fait que l'opération d'instanciation des lambda corps

en substituant le paramètre formel est l'opération fondamentale de notreimplémentation, on montrera comment la rendre plus e�cace.

On décrit comment transformer les lambda expressions en supercombi-nateurs qui sont facilement instanciables. Cette transformation est appeléelambda lifting 2.

2ce terme a été utilisé pour la première fois par H. Curry dans son introduction à lalogique combinatoire.

Page 80: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 73

3.5.1 Utilité des supercombinateursL'opération d'instanciation décrite précédemment sous le nom instancier

fait un parcourt récursif de tout l'arbre représentant le lambda corps. Unetelle opération est relativement ine�cace car :

� Il y'a toujours une analyse du type de chaque noeud de l'arbre.� A chaque noeud représentant une variable il y'a un test si le noeud est

le paramètre formel ou non.� De nouvelles instances de sous expressions qui ne contiennent pas d'oc-

currences libres du paramètre formel de l'abstraction sont construites,alors qu'elles peuvent être simplement partagée , en raison de leursinvariance .

� Au niveau de chaque lambda abstraction il y'a construction d'une nou-velle instance, par exemple pour l'expression (λx.(λy.(+ x y)) 5 6),il y'a construction d'une instance avec substitution de x par 5 ce quidonne, (λy.(+ 5 y) 6), puis construction d'une autre instance avec ysubstitué par 6. La première instance construite est inutilisable puis-qu'il y'a construction d'une autre instance �nale.

On pourrait substituer plusieurs paramètres à la fois, dans une seule opé-ration d'instanciation.

Le souci principal est le temps d'exécution, et la taille de la mémoire uti-lisée, on pourrait alors envisager de compiler les lambda abstractions en uneséquence d'instructions qui produisent une instance du lambda corps une foisexécutées, l'opération d'instanciation consiste alors à évaluer simplement laséquence d'instruction associée à la lambda abstraction.

Ainsi l'exécution de notre code compilé est plus rapide que l'opérationinstancier car :

� Tout les tests e�ectués par l'opération instancier lors de l'évaluationsont e�ectués en avance par le compilateur, c-à-d lors de la compilation(compile-time), ce qui réduit le temps d'évaluation (run-time).

� Pour les mêmes raisons aussi, qu'un code compilé est exécuté plus ra-pidement qu'un code interprété dans les langages conventionnels.

L'idée de la compilation est une bonne idée, cependant il y'a un pro-blème, c'est l'occurrence des variables libres dans les lambda abstractions,par exemple, considérons la lambda abstraction :

λx.λy.(+ x y)

Page 81: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 74

Chaque application de la λx abstraction à un argument di�érent produitune nouvelle λy abstraction di�érente, mais un code �xé associé à la λy abs-traction produirait une seule et unique instance, une solution consiste alorsà rendre la valeur de x accessible par le code produit pour la λy abstraction.

Cette approche nous conduit à la machine SECD [Gla90], où le code gé-néré pour les lambda abstractions peut accéder à une sorte d'environnementqui contient la valeur de chacune des variables libres, nous donnant ainsi lapossibilité de produire pour chaque lambda abstraction une seule et uniqueséquence de code �xé (cette technique est utilisé aussi par les langages struc-turés) .

Dans notre implémentation on utilisera une technique plus simple et pluse�cace qui consiste à transformer les lambda abstractions dans une formespéciale où elle ne contiennent pas de variables libres (supercombinateurs),c-à-d que le code compilé n'aura pas besoin d'accéder à un environnement,puisque les supercombinateurs ne contiennent pas de variables libres.

3.5.2 Réduction par plusieurs argumentsSuivant l'exemple ((λx.λy.+ x y) 5 6), le problème de l'occurrence libre

de x dans le λy corps peut être résolu en utilisant une forme modi�ée de βréduction, dans laquelle on pourrait e�ectuer plusieurs β réductions à la fois,Ce qui donne :

→ (λx.λy.+ x y) 5 6

→ + 5 6

au lieu de :

→ (λx.λy.+ x y) 5 6

→ (λy.+ 5 y) 6

→ + 5 6

En plus, cette forme de β réduction économise la perte d'espace mémoireen évitant la construction d'instances intermédiaires des lambda expressions.

3.5.3 Implémentation des supercombinateursLa stratégie de compilation est de transformer toute les lambda abstrac-

tions en des supercombinateurs en se basant sur ce qui suit :

Page 82: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 75

� On donnera à chaque supercombinateur un nom unique par souci declarté, car non seulement les lambda abstractions sont des fonctionsanonymes mais elle peuvent, chacune, générer une multitude de super-combinateurs totalement di�érents.

� Une lambda expression est transformée en :1. un ensemble de supercombinateurs2. une expression à évaluer

Pour di�érencier les supercombinateurs des variables ceux-ci sont précé-dés par $.

Par exemple, on veut représenter l'expression

(λx.λy.+ x y) 5 6

en

$f x y = + x y → un ensemble de supercombinateurs

�������$f 5 6 → une expression a evaluer

La di�érence avec les lambda abstractions est que les supercombinateurspossèdent une arité (un nombre d'argument). Une application d'un supercom-binateur ne peut être considéré comme un redex si le nombre d'argumentsdisponibles est insu�sant.

Par exemple ($f 5) dans l'exemple précédent n'est pas un redex alors que($f 5 6) en est un.

3.5.4 Algorithme du lambda liftingConsidérons l'exemple suivant :

(λx.(λy.+ y x) x) 4

La lambda abstraction la plus interne (λy. + y x) possède une variablelibre x, donc n'est pas un supercombinateur.

Une simple transformation va permettre d'obtenir un supercombinateur :faire de chaque variable libre un extra paramètre (appelée abstrac-tion des variables libres)

Page 83: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 76

Ainsi on transforme

(λy.+ y x)

en

(λw.λy.+ y w) x

La lambda abstraction (λw.λy. + y w) est un supercombinateur, donc,l'expression précédente devient :

(λx.(((λw.λy.+ y w) x) x)) 4

En donnant un nom $Y à notre abstraction transformée :

$Y w y = + w y

�������(λx.$Y x x) 4

Sachant aussi que la λx abstraction est elle aussi un supercombinateur,noté $X, on a

$Y w y = + w y

$X x = $Y x x

�������$X 4

Il faut préciser que la lambda abstraction transformée n'est pas remplacéedans l'expression par son nom uniquement, mais par une application de sonnom aux variables libres liftées.

L'exécution du programme consiste alors à une série de réécritures :

→ $X 4

→ $Y 4 4

→ + 4 4

→ 8

En conclusion, l'algorithme de lambda lifting est :

Page 84: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 77

tant que il existe des lambda abstractions faire1. choisir une lambda abstraction qui est la plus interne (ne pos-

sédant pas de lambda abstractions dans son corps).2. faire de ses variables libres des extra paramètres.3. donner un nom arbitraire a la lambda abstraction transfor-

mée.4. remplacer l'occurrence du lambda abstraction par son nom

appliquée aux variables libres.5. compiler la lambda abstraction et associer son nom au code

compilé.�n

3.5.5 Stratégies d'évaluation des supercombinateursÉtant donné la manière de compiler un programme en un ensemble de

supercombinateurs, il existe tout un spectre de stratégies pour évaluer cessupercombinateurs :

� On peut garder le corps des supercombinateurs sous forme d'arbre, etl'instancier en utilisant une fonction similaire à celle décrite plus haut(instancier) en utilisant tous les mécanismes décrits plus haut, telsque la sélection du prochain redex, la réduction, l'utilisation de la pilemémoire et les noeuds d'indirection, avec une légère di�érence dans leslambda abstractions qui deviennent des supercombinateurs (absence devariables libres). Une légère complication apparaît car la réduction sefait par substitution sur plusieurs paramètres à la fois.

� On peut garder le corps des supercombinateurs sous forme d'arbre,mais stocké dans un espace contigu. Ainsi l'opération de substitutiondevient un simple et rapide parcours de l'espace contigu représentantle supercombinateur au lieu d'un long parcours récursif se déplaçant àtravers des pointeurs (cette technique à été utilisée par Keller dans sonimplémentation d'un réducteur de graphes parallèles [Pey87]).

� On peut compiler le corps des supercombinateurs en une séquence d'ins-tructions qui, une fois exécutées, permettent de construire une instancedu supercombinateur. Objet de notre implémentation, cette idée à étéimaginé par Johnson pour l'implémentation d'un compilateur du lan-gage Lazy ML [Joh85].

Page 85: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 78

SK-compilation évaluationexpressions code SK resultat- - -

Nous présentons, avant d'aborder la G-machine qui est l'objet de notreimplémentation, une technique d'évaluation (machine SK) simple, basée surun ensemble �xe de supercombinateurs. Nous présentons aussi ses avantageset ses inconvénients, pour justi�er notre choix de la G-machine.

3.6 Machine SKDans cette implémentation on reprendra depuis la phase où l'expression

à évaluer a été transformée jusqu'au lambda calcul ordinaire, on utilisera unetechnique de réduction de graphe basée sur un ensemble �xe de supercom-binateurs, et ceci à l'opposition de l'approche décrite précédemment danslaquelle les dé�nitions de supercombinateurs sont générées par le programmeen utilisant le lambda lifting [Hao99] [Pey87].

3.6.1 SK CombinateursS, K sont les combinateurs de base dans l'ensemble �xe des supercombi-

nateurs utilisés dans cette implémentation.L'idée est de transformer le programme en un autre ne contenant que les

fonctions prédé�nies et les constantes avec les combinateurs S, K, I (c-à-dl'élimination des lambda abstractions et des variables liées) sachant que :

S f g x → f x (g x)

K x y → x

I x → x

Cette méthode est e�cace car elle donne lieu à une machine de réductionextrêmement simple.

3.6.2 S-TransformationElle consiste à passer de la lambda expression

(λx.e1 e2)

Page 86: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 79

à la lambda expression :

S (λx.e1) (λx.e2)

On peut véri�er que : (λx.e1 e2) ≡ S (λx.e1) (λx.e2) en substituant Ssur le coté droit avec S = (λf g x.f x (g x)).

Exemple :λx.and x True

si on applique la S-transformation on aura :

λx.and x True→ S (λx.and x) (λx.True)

→ S (S (λx.and) (λx.x)) (λx.True)

Le λx s'enfonce d'un niveau à chaque fois qu'on e�ectue une S-transformationet ceci tant qu'il reste des lambda abstractions qui contiennent dans leur corpsdes applications

A la �n, les corps des lambda abstractions sont des objets atomiques eton peut cependant considérer deux cas :

� des expressions de la forme λx.x� des expressions de la forme λx.c (c 6= x syntaxiquement)

d'où les transformations qui suivent.

3.6.3 I-Transformation :Elle consiste à remplacer chaque lambda abstraction de la forme (λx.x)

par I sachant que I est dé�nie par I x = x.

L'exemple précédant donne :

S (S (λx.and) (λx.x)) (λx.True)→ S (S (λx.and) I) (λx.True)

Page 87: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 80

3.6.4 K-TransformationCelle-ci consiste à remplacer chaque lambda abstraction de la forme λx.c

par (K c), où K est dé�nie par :

K c x → c

C'est la fonction qui quelque soit sont deuxième argument elle l'écarte etretourne son premier argument, c'est-à-dire K c = (λx.c).

Exemple :On reprend l'exemple précédent :

λx.and x True→ S (S

K and︷ ︸︸ ︷(λx.and) I)

K True︷ ︸︸ ︷(λx.True)

→ S (S (K and) I) (K True)

Ainsi on arrive à écrire l'expression sans lambda abstractions ni variables.Pour résumer, les transformations suivantes ont été développées :

(λx.e1 e2) → S (λx.e1) (λx.e2)

(λx.x) → I

(λx.c) → K c

(3.27)

Et les réductions suivantes :

S f g x → f x (g x)

I x → x

K c y → c

(3.28)

Dans l'implémentation de ce schéma, S, K, I ne seront considérés quecomme étant des fonctions primitives telles que +, −, . . .

Considérons l'exemple complet :

l'expression

(λx.+ x x) 5

Page 88: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 81

produit :

(λx.+ x x) 5 → S (λx.+ x) (λx.x) 5

→ S (S (λx.+) (λx.x)) (λx.x) 5

→ S (S (K +) I) I 5

et son évaluation donne, (en évaluant par ordre normal de réduction) :

S (S (K +) I) I 5 → (S (K +) I) 5 (I 5)

→ K + 5 (I 5) (I 5)

→ + 5 (I 5)

→ + 5 5

→ 10

3.6.5 Schéma �nal de la SK-CompilationLa SK-compilation est représentée par deux fonctions Compile et Abs-

tract, dé�nies par :

Compile[e1 e2] = Compile[e1] Compile[e2]Compile[λx.e] = Abstract x [Compile[e]]Compile[cv] = cv

Abstract x [f1 f2] = S (Abstract x [f1]) (Abstract x [f2])Abstract x [x] = I

Abstract x [cv] = K cv

où :e, e1, e2, f1, f2 sont des expressions en lambda calculx est variablecv est variable ou constante (y compris les fonctions prédé�nies telles que +)

Page 89: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 82

3.6.6 Avantages et inconvénients de la machine SKLa technique des SK combinateurs est une des techniques de réduction

les plus récentes. Implémentée pour la première fois par D. Turner. Commetout autre technique, elle possède les avantages et les inconvénients que nousallons présenter :

i. Avantages� Un ensemble �xé de combinateurs peut être implémenté directement en

matériel (hardware), évitant un niveau supplémentaire d'interprétation.� L'instanciation des corps de lambda abstractions est faite en mode

paresseux, évitant la manipulation de parties non nécessaires (section3.4.2).

� La machine à réduction est relativement simple à implémenter.

ii. Inconvénients� La taille du code SK généré est relativement importante. Cependant, on

peut envisager d'étendre l'ensemble SK par de nouveaux combinateursdotés de nouvelles fonctions, réduisant ainsi la taille du code généré).

� La compilation du lambda calcul au code SK demande une perfor-mance, que ce soit au niveaux de la taille mémoire ou au niveau de larapidité d'exécution.

3.7 G-Machine3.7.1 Introduction

Le coeur de chaque réducteur de graphe réside dans l'opération d'instan-ciation, alors pour la rendre plus performante, il faut trouver une manièreplus rapide que le parcours du graphe avec beaucoup de tests. Suivant ce prin-cipe on va examiner la G-machine qui est le résultat de recherches récentesdans le domaine des compilateurs de langages fonctionnels [Pey87].

L'idée consiste à transformer chaque supercombinateur en un ensembled'instructions séquentielles qui permettent de construire une instance de cegraphe machinalement. Sachant que la G-machine à été imaginée pour êtreconstruite au niveau matériel, on présentera une machine séquentielle abs-traite.

Notre description de la G-machine se divise en deux parties :

Page 90: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 83

1. Une description de l'algorithme de compilation qui transforme les graphesde supercombinateurs en un ensemble d'instructions appelé G-code.

2. Une description du G-code et de chaqu'une de ces instructions.

3.7.2 Exemple d'exécutionConsidérons la fonction :

$f g x = $K (g x)qui est un supercombinateur, le code généré pour $f est :

PUSH 1PUSH 1MKAPPUSHGLOBAL $KMKAPSLIDE 3UNWIND

Dans la �gure 3.5, le diagramme (a) représente l'état de la machine avantl'exécution de la séquence d'instructions de $f, la selection du prochain re-dex se passe comme dans ce qui à été décrit précédemment, les deux élé-ments avant le sommet de la pile représentent des pointeurs vers les noeudsd'application dont les parties droites sont les expressions qui remplacent lesarguments g et x.

l'instruction PUSH utilise un adressage relatif au sommet de la pile. Igno-rant le pointeur vers le noeud de $f, le premier élément de la pile est repre-senté par 0, le suivant est 1 et ainsi de suite.

Le diagramme (b) montre la pile modi�ée après l'execution de PUSH 1, c-à-d un empilement de l'élément x dans la pile, x étant à la seconde position àpartir du sommet de la pile. Après une autre instruction PUSH 1, un pointeurde g se trouve au sommet de la pile, l'état de la machine est alors représentédans (c).

Le diagramme (d) montre l'exécution de l'instruction MKAP, elle prend lesdeux pointeurs à partir du sommet de la pile et forme un noeud d'applicationa partir de ces deux pointeurs, et le met dans la pile.

Le diagramme (e) montre l'exécution de l'instruction PUSHGLOBAL $K, quia pour e�et un empilement d'un pointeur vers le supercombinateur $K danla pile.

Une instruction MKAP complète l'instanciation du corps de $f comme mon-tré dans le diagramme (f).

Maintenant on remplace l'expression originale, $f g x, par la nouvelleinstance du corps, $K (g x).

Page 91: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 84

.....................

��.....................

��.....................

.....................

.....................

.....................

-

-

6

6-6--..........................................

..........................................

AA

JJJJJJ

.............

6

..........................6

-

-

-

-

-

-

-

-

(d) MKAP(c) PUSH 1(b) PUSH 1(a)

fff@

f ggggxxxx@ @ @ @

@@@@

-

-

.....................

���

���

-

--

-

- -

-

............. 6.....................

6

.....................

6

.............

6

-....................................................................................BB

BBAA

AA

(g) SLIDE 3(f) MKAP(e) PUSHGLOBAL $K

KK

@ff

@ @ @x x@ @

gg@

@

K @g x

Fig. 3.5 � Exécution du code pour le supercombinateur $f

Page 92: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 85

L'instruction SLIDE 3 fait glisser le resultat de trois cases dans la pileremplaçant ainsi l'expression originale (diagramme (g)).

L'instruction UNWIND permet de continuer l'évaluation.

3.7.3 Syntaxe du langage sourceLe langage source du G-compilateur est de la syntaxe :

<E> : := <constante>| <identificateur>| <E> <E>| let <identificateur>=<E> in <E>| letrec <identificateur>=<E>

...<identificateur>=<exp>

in <exp>Il est nécessaire de préciser la syntaxe du langage source parce que notre

compilateur a besoin de traiter chaque cas di�éremment, en se référant à lasyntaxe il est facile de distinguer les di�érents cas possibles.

La compilation se fait de la manière :

$F . . . = . . .$G . . . = . . ....−−−−−−−$Prog

→ G-compilation →

$F = code de $F . . .$G . . . = code de $G . . ....−−−−−−−code de $Prog

3.7.4 État de la pile Durant l'exécutionSupposons que la G-machine va exécuter l'expression ($F a b c d), avec

$F un supercombinateur d'arité 2. Avant la réduction la pile est de la formereprésentée dans la �gure 3.6

Pour que les arguments de F, (a et b), deviennent accessibles via la pile,il faut réarranger cette pile, en changeant les pointeurs vers les noeuds d'ap-plications par leur cotés droits (�gure 3.6).

De cette manière la pile ressemble à la description de la �gure 3.7

Page 93: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 86

@$F a

@b

@c

@d

�� @@

�� @@

�� @@

�� @@

-

-

-

-

-

sommet

a

@b

@c

@d

�� @@

�� @@

�� @@

-

-

-

-

-

sommet

Fig. 3.6 � Rearrangement de la pile

...

Arg 1

...

Arg nracine du redex

...

contextecourant

valeurs intermédiaires

Fig. 3.7 � État de la pile pendant l'exécution

Page 94: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 87

3.7.5 Les Schémas de Compilation F et RAvant de donner une description des schémas de compilation on a besoin

de quelques dé�nitions :

� p : la liste de liens. Autrement dit, p est une fonction à laquelle ondonnera une variable et elle retourne la position de l'expression liée àcette variable dans la pile, par exemple :

si p = [x← 1, y ← 2] alors p x = 1

� d : la longueur du contexte courant.

F est le schéma de compilation d'une dé�nition d'un supercombinateur.Nous pouvons donner une dé�nition du schéma de compilation F :

F[f x1 . . . xn = E] = GLOBSTART f, n;

R[E] [x1 ← n, x2 ← n− 1, . . . , xn ← 1] n (3.29)

où f est le nom du supercombinateur, GLOBSTART f, n est une pseudo-instruction qui déclare une fonction nommée f ayant l'arité n.

F appelle une fonction R pour compiler le code du corps de f en luipassant l'expression E comme argument, ainsi que les p et d courants.

Avant de décrire le schéma R, il faut noter que le code d'un supercombi-nateur est divisé en 4 parties :

1. Construction d'une instance du corps d'un supercombinateur en utili-sant les paramètres dans la pile.

2. Mise à jour de la racine du redex par l'instance construite.3. E�acement des paramètres dans la pile.4. Lancement de la prochaine réduction.

Ces parties conduisent au schéma suivant :

R[E] p d = C[E] p d; UPDATE (d+ 1); POP d; UNWIND (3.30)

avec :C génère un code qui construit l'instance du supercombinateur et met unpointeur de celle-ci au sommet de la pile

Page 95: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 88

UPDATE (d+1) déplace le sommet de la pile vers la (d+1)eme position dansla pile à partir du sommet (mise à jour de la racine de redex par l'instanceconstruite)

POP d enlève d dernier éléments dans la pile,UNWIND lance la prochaine réduction.

3.7.6 Le Schéma de Compilation CLe schéma de compilation C reçoit 3 arguments : l'expression à compiler

en plus de p et d qui spéci�ent où trouver les arguments du supercombina-teur dans la pile, et produit un code permettant de construire l'instance del'expression avec substitution des paramètres formels.

Selon la syntaxe du langage source l'expression à compiler peut prendreplusieurs formes.

Chaque forme sera traitée dans les cas suivants :

i. E est une Constante :Il y a deux cas :

� E est un entier (caractère, ...) : dans ce cas tout ce dont on a besoinest d'empiler un pointeur vers cet entier

C[i] p d = PUSHINT i (3.31)

� E est un supercombinateur ou une fonction primitive dite f : une ins-truction similaire permet de mettre un pointeur de f dans la pile :

C[f ] p d = PUSHGLOBAL f (3.32)

ii. E est une Variable :La valeur de la variable, dite x, se trouve dans la pile à la position (d−p x)

du sommet, donc :

C[x] p d = PUSH (d− p x) (3.33)

Page 96: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 89

iii. E est une Application :Si E est une application (E1 E2) alors la construction de celle-ci revient

à construire E2 (laissant un pointeur de l'instance construite au sommet dela pile), ensuite, construire E1 de la même manière que E2. Finalement créerun noeud d'application à partir de E1 et E2 :

C[E1 E2] p d = C[E2] p d; C[E1] p (d+ 1); MKAP (3.34)

On note que le contexte courant dans la pile est plus profond d'une caseaprès la première construction de E2.

MKAP est une instruction qui prend deux éléments au sommet de la pileet construit le noeud d'application de ces deux éléments, les dépile et empilele nouveau noeud d'application.

iv. E est une let expression :Pour compiler une expression de la forme :

let x = Ex in E

il faut :1. Construire une instance de Ex, en laissant un pointeur de celle-ci au

sommet de la pile.2. Augmenter p pour que x devienne accessible via la (d + 1)me position

dans la pile.3. Construire une instance de E, en utilisant les nouvelles valeurs de p et

de d, laissant un pointeur de l'instance construite dans la pile.4. On a un pointeur de E au sommet de la pile. Avant le sommet, il y a

un pointeur vers Ex, puisque Ex est maintenant inutile on glissera lepointeur de E à la place de Ex.

C[let x = Ex in E] p d = C[Ex] p d; C[E] p [x← (d+ 1)] (d+ 1); SLIDE 1(3.35)

On note que p [x← (d+1)] veut dire : p augmenté avec le lien x← (d+1).SLIDE n est une instruction qui permet de déplacer le sommet de la pile

à la position n.

Page 97: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 90

v. E est une letrec expression :Considérons une expression de la forme :

letrec D in E

où D est un ensemble de dé�nitions, E est une expression.Rappelons juste qu'une expression letrec dans un supercombinateur est

une description d'un graphe cyclique. Pour construire un tel graphe, il faut :1. Allouer un ensemble de cellules vides, une pour chaque dé�nition, et

mettre des pointeurs vers ces cellules dans la pile.2. Augmenter le contexte p et d pour que les valeurs des variables locale-

ment dé�nies soient accessibles à partir de la pile.3. Pour chaque dé�nition :

(a) Construire une instance de celle-ci, laissant un pointeur vers l'ins-tance au sommet de la pile.

(b) Mettre à jour la cellule vide correspondante avec l'instance qui setrouve au sommet de la pile.

4. Construire une instance deE, laissant un pointeur de celle-ci au sommetde la pile.

5. Finalement, glisser le sommet de la pile de n positions où n est lenombre de dé�nitions.

Le schéma devient :

C[letrec D in E] p d = CLetrec[D] p d; C[E] p d; SLIDE (d− d) (3.36)où :

p = p

x1 ← d+ 1...xn ← d+ n

et d = d+ n

avec :

CLetrec

x1 = E1

x2 = E2...xn = En

p d =

ALLOC n;

C[E1] p d; UPDATE n;

C[E2] p d; UPDATE (n− 1);...C[En] p d; UPDATE 1;

Page 98: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 91

3.7.7 Schéma �nal de la G-compilationVoici le schéma �nal :

F[f x1 . . . xn = E] = GLOBSTART f, n;R[E] [x1 ← n, x2 ← n− 1, . . . , xn ← 1] n

R[E] p d = C[E] p d; UPDATE (d+ 1); POP d; UNWIND

C[i] p d = PUSHINT i

C[f ] p d = PUSHGLOBAL fC[x] p d = PUSH (d− p x)C[E1 E2] p d = C[E2] p d; C[E1] p (d+ 1); MKAPC[let x = Ex in E] p d = C[Ex] p d;

C[E] p [x← (d+ 1)] (d+ 1); SLIDE 1

C[letrec D in E] p d = CLetrec[D] p d; C[E] p d; SLIDE (d− d)

avec :

p = p

x1 ← d+ 1...xn ← d+ n

et d = d+ n

et :

CLetrec

x1 = E1

x2 = E2...xn = En

p d =

ALLOC n;C[E1] p d; UPDATE n;C[E2] p d; UPDATE (n− 1);...C[En] p d; UPDATE 1;

3.7.8 Optimisation du G-codePour augmenter les performances de la G-machine, il est nécessaire de

faire quelques optimisations au code généré, telles que l'élimination des ins-

Page 99: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 92

tructions inutiles et l'extension du jeu d'instructions avec de nouvelles ins-tructions plus spécialisées.

Voici quelque règles d'optimisations du G-code généré :

1. une séquence d'instructions MKAP est remplacée par MKAP n

MKAP ;MKAP ; ... ;MKAP ;︸ ︷︷ ︸n

⇒ MKAP n ;

2. deux instructions SLIDE sont remplacée par une seule instruction SLIDE

SLIDE n ; SLIDE m ; ⇒ SLIDE (n+m) ;

3. deux instructionsMKAP sont remplacée par une seule instructionMKAP

MKAP n ; MKAP m ; ⇒ MKAP (n+m) ;

4. une instructionMKAP suivie d'une autre instruction UPDATE sont rem-placées par une nouvelle instruction UPDAP

MKAP 1 ; UPDATE p ; ⇒ UPDAP p

en général :

MKAP (n+1) ; UPDATE p ⇒ MKAP n ; UPDAP p

3.7.9 Fonctions prédé�nisLes fonctions prédé�nis (primitives) sont implémentées de trois manières :

1. En langage fonctionnel dans une librairie standard (stdlib) , par exemple :

map f [] = []map f (x:xs) = f x : map f xs

2. Implémentées sous forme de G-code3. Implémentées en langage source (de développement) formant une li-

braire d'exécution (run-time library).

Page 100: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 93

3.7.10 Extension du jeu d'instructionsi. L'instruction EVAL

La plupart des fonctions prédé�nis sont strictes, elles nécessitent une éva-luation des arguments avant l'évaluation de leurs corps, pour cela les fonctionsprédé�nis et implémentées en G-code sont munis d'une nouvelle instructionEVAL, qui a pour tache de :

1. examiner l'objet pointé par le sommet de la pile , si c'est un construc-teur , un entier , un caractère ..., même un supercombinateur , ou unefonction prédé�nie , ne rien faire .

2. si c'est un noeud d'application , créer une nouvelle pile , copier le noeuddans la nouvelle pile , sauvegarder le contexte actuel , puis lancer uneexécution d'instruction UNWIND.

ii. $IF,$CASE-T et l'instruction JUMPPour générer le code de $IF, on a besoin de nouvelles instructions de

branchement : TEST et JUMP.ainsi le code de $IF est :

PUSH 0 ; obtenir le premier argumentEVAL ; l'évaluerTEST True L1 ; branchement a L1 si c'est FALSEPUSH 1 ; obtenir le second argumentJUMP L2 ;

L1: PUSH 2 ; obtenir le troisième argumentL2: EVAL ; évaluation avant la mise a jour

UPDATE 4 ; mise a jour de la racine du redexPOP 3; élimination des arguments dans la pileUNWIND ; continuer l'évaluation

( L1 et L2 sont des addresses d'instructions )

on peut même donner une interprétation au supercombinateur $CASE-T(section 3.3.3) sachant que :

CASE-T (si a1 a2 . . . ari) b1 b2 . . . bi . . . bn = bi

sa compilation en G-code donne :

PUSH 0 ; obtenir le premier argumentEVAL ; l'évaluer

Page 101: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 94

TEST S1 L1 ; comparer avec le 1er constructeurPUSH 1 ; obtenir le second argumentJUMP L(n+1) ;

L1: TEST S2 L2 ; comparer avec le 2ème constructeurPUSH 2 ; obtenir le troisième argumentJUMP L(n+1) ;...

L(n-1): TEST Sn Ln ; comparer avec le n-ième constructeurPUSH n ; obtenir le (n+1)-ème argumentJUMP L(n+1) ;

Ln: PUSHGLOBAL $FAIL ; mettre la valeur de FAIL dans la pile

L(n+1): EVAL ; évaluation avant la mise a jour

UPDATE (n+2) ; mise a jour de la racine du redexPOP (n+1) ; élimination des arguments dans la pileUNWIND ; continuer l'évaluation

avec S1 S2 ... Sn constructeurs de type T.

Par exemple la compilation de :

CASE-LIST x b1 b2

donne :PUSH 0 ; obtenir le premier argumentEVAL ; l'évaluerTEST CONS L1 ; comparer avec le 1er constructeurPUSH 1 ; obtenir le second argumentJUMP L3

L1: TEST NIL L2 ; comparer avec le 2ème constructeurPUSH 2 ; obtenir le troisième argumentJUMP L3 ;

L2: PUSHGLOBAL $FAIL ; mettre la valeur de FAIL dans la pileL3: EVAL ; évaluation avant la mise a jour

UPDATE 4 ; mise a jour de la racine du redexPOP 3 ; élimination des arguments dans la pileUNWIND ; continuer l'évaluation

Page 102: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 95

3.8 ConclusionCe chapitre a traité la conception d'un compilateur dans ses deux étapes :

� L'étape de translation des di�érentes expressions en lambda calcul or-dinaire.

� L'étape d'évaluation de ces expressions suivant les techniques déjà dé-crites, et ce jusqu'à l'obtention de leur résultat.

Nous avons choisi d'adopter la technique de la G-machine. Ce choix à étémotivé par le fait que cette approche s'implémente facilement sur les ma-chines séquentielles. D'autre parts, la G-machine peut produire son proprecode.

Notre implémentation intègre a la fois la compilation (production du G-code) et l'évaluation du code généré.

La �gure 3.8 récapitule toutes les étapes et choix de translation et d'éva-luation établies dans le chapitre de la conception.

Page 103: Compilateur orienté lambda calcul

CHAPITRE 3. CONCEPTION DU COMPILATEUR 96

'&

$%Programme fonctionnel

?

Translationen λ calcul enrichi

?

Translationen λ calcul simple

?'&

$%

Expressionsen λ calcul simple

?

?

?

SK compilation

?

Execution du code SK

λ lifting (translationen supercombinateurs)

?

G-compilation

?

Evaluation parG-machine

Instantiation directedes λ expressions

-

-

Evaluation

-

Compilation

--

-

Translation

Fig. 3.8 � Di�érentes étapes de translation et de compilation d'un programmefonctionnel

Page 104: Compilateur orienté lambda calcul

Chapitre 4

Implémentation du compilateur

Le resultat �nal de notre implémentation est un environnement de pro-grammation fonctionnelle supportant un langage beaucoup inspiré des lan-gages tels que Haskell [Pey99] et de Miranda [Tur86], ce langage inclut touteles caractéristiques utilisées dans les langages fonctionnels purs : le supportde l'évaluation souple (section 3.4) et de la curry�cation (section 2.2), unsystème de typage statique, les types structurés (ou algébriques [Pey99]), lepattern matching, les listes de compréhension et un ensemble de types stan-dards incluants les listes et les uples.

La première version a été compilée sous le système d'exploitation Linux(noyau 2.2.14) par le compilateur standard gcc de GNU. Ce chapitre décritles techniques et les idées principales utilisées dans l'implémentation de notreenvironnement, en plus il décrit certains détails ou problèmes rencontréspendant l'implémentation et qui ne peuvent pas être remarqués durant laconception (règle de disposition syntaxique, associativité des opérateurs...).

4.1 Objectifs de l'implémentationDurant l'implémentation on a �xé certains objectifs, parmi ces objectifs,

il y a :La compatibilité avec le langage Haskell : La compatibilité avec un tel

langage est très importante parce que non seulement il est actuellementle langage fonctionnel le plus répandu, mais en plus il a été réalisé pourfaire face aux problèmes des anciens langages fonctionnels qui n'ont pasencore connut le succès des langages impératifs [Pey99] (des problèmesconcernant surtout la représentation de entrés/sorties dans des langagesqui n'acceptent pas les e�ets de bord). La compatibilité avec le langageHaskell n'est pas atteinte à 100%, mais nous espérons l'atteindre avec

97

Page 105: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 98

le temps, et une collaboration d'autres developpeurs sur internet endistribuant librement le code source de l'implémentation.

La portabilité : Pour que le système soit utilisé par un grand nombre depersonnes il faut qu'il soit implémenté sur plusieurs plateformes ; surce point, le code a été écrit pour être compilé au moins sous les deuxplateformes : Unix (incluant Linux ) et Windows.

La simplicité et l'extensibilité : Pour maintenir une extensibilité du codesource, l'environnement de programmation se lance sur deux processusqui s'exécutent en parallèle et qui communiquent entre eux à travers lesystème d'exploitation. Le premier processus a pour tâche l'analyse desprogrammes, la compilation et l'évaluation du code généré ; le deuxièmereprésente une interface avec l'utilisateur contenant des outils d'aide àla programmation, de cette manière le développement et la maintenancedu projet reste clair. En e�et, chaque partie du projet a été épargnéedes complications apportées par l'autre.

4.2 Environnement de développement4.2.1 Choix du langage

Le choix du langage d'implémentation a été l'une des décisions les plusdi�ciles, entre des candidats tel que C ou C++ et Haskell car chaque langageo�re des raisons très convaincantes pour l'utiliser :

1. Il est facile de faire une conception orienté objet, sachant qu'il n y'apas mieux que la notion d'objet pour représenter des graphes nécessi-tant une manipulation complexe pour les transformer, et dans ce casl' utilisation du C++ avec sa notion des classes, de l'héritage, et desfonctions virtuelles paraît très convenable.

2. Il est aussi facile de développer notre système avec un langage fonc-tionnel, fortement typé tel que Haskell ; en e�et, sa structure et sescaractéristiques rapprochent les deux étapes essentielles de la réalisa-tion d'un projet : celle, de la conception et celle de la mise en oeuvre,sachant qu'une bonne implémentation qui ne contient pas de `bogues'est une implémentation qui est en cohérence avec les règles établies auniveau de ça conception [Hud94].En plus, certains compilateurs du langage Haskell (comme le GlasgowHaskell implémenté par P. Jones et P. Wadler) ont été réalisés en Has-kell lui même, comme preuve de la puissance et de l'utilité d'un tellangage dans la production de logiciel [Pey93].

Page 106: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 99

Cependant, le langage d'implémentation choisi a été le C, pour les raisonssuivantes :

1. Tout en étant un langage de haut niveau, le C permet de faire desmanipulations d'un niveau très bas, ce qui o�re une liberté de gestionde la mémoire au programmeur sachant que notre implémentation doitêtre en mesure de gérer des programmes de tailles importante. Autre-ment dit, notre implémentation doit manipuler des graphes de plus enplus importants et de plus en plus complexes, pour cela tous les typesde noeuds (appelés cellules dans l'implémentation) utilisés pour repré-senter les graphes doivent être de taille minimale tout en conservantl'information nécessaire.Sachant qu'en Haskell, la gestion de la mémoire est automatique, etqu'en C++ la taille des objets de classes n'est pas contrôlée par leprogrammeur, mais par le compilateur, et ceci pour l'exploitation de lanotion du polymorphisme et des fonctions virtuelles [Del94]. Ainsi, lamanipulation des données en C++ n'est pas d'aussi bas niveau qu'enlangage C.

2. Malgré la puissance et le niveau d'abstraction o�erts par le langage Has-kell, les programmes en ce dernier s'exécutent toujours moins rapide-ment que leurs équivalents en C, pour la raison suivante : un programmeen langage impératif, qui est un ensemble d'instructions séquentiellesse compile facilement en un code exécuté par une machine séquentielle,ce qui n'est pas le cas pour un langage déclaratif tel que le Haskell.

4.2.2 Choix des outils de développementPour le développement on a utilisé des outils distribués librement sous

licence GPL (General Public License), tel que le compilateur gcc, le généra-teur d'analyseurs syntaxiques bison, le débogueur gdb, la gestion du Make�leavec make et les outils de con�guration autoconf et automake. L'ensemble deces outils de développement a été géré par l'environnement de développementintégré sous Linux KDEVELOP 1.2 [Nol99] (disponible sous licence GPLau site http://www.kdevelop.org).

L'interface graphique a été développée en C++ en utilisant la bibliothèqued'interfaçage graphique QT/KDE version 1.4 et des outils de design visueltel que qtDesigner, développées par trolltech (disponibles sous licence GPLau site http://www.trolltech.com/qt)

Page 107: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 100

6? 6? 6? 6? 6?

- - - -

mem.c : gestion de la mémoire

input.c :Analyselexicale

et syntaxique

static.c :Analysestatique

type.c :Typage ettranslation

compiler.c :Translation etcompilation

en supercomb.

gmachine.c :Exécution sur

machineabstraite

Fig. 4.1 � Structure de l'implémentation

4.3 Structure de l'implémentationLes composants principaux de l'implémentation sont illustrés par le dia-

gramme dans la �gure 4.1, qui montre aussi d'une certaine façon comment lesprogrammes sont traités en partant de l'analyse lexicale jusqu'à l'exécutiondans la machine abstraite en passant par la translation et la compilation.

Chaqu'un de ces composants est implémenté dans un module correspon-dant en C. Chaque partie est responsable de l'allocation et de l'initialisa-tion des variables et des structures de données dont elle dépend, à traversune fonction de contrôle a chaque composant, par exemple dans le modulegmachine.c, la fonction de contrôle est appelée machine(), on peut l'uti-liser par exemple pour le lancement de l'opération d'initialisation a traversles données de ce module grâce à l'appel machine(INSTALL). Plusieurs mes-sages peuvent ainsi être passés aux di�érents composants du système commeINSTALL pour l'initialisation, MARK pour le marquage des données locales a�nde les préparer pour l'opération du ramassage des miettes, et EXIT pour la li-bération des espaces mémoires alloués avant la �n du programme. La fonctioneverybody() est utilisée pour envoyer un message particulier à chaque com-posant du système, par exemple l'une des premières fonctions appelées durantle lancement du programme est everybody(INSTALL), et l'une des dernièresfonctions appelées durant la �n du programme est everybody(EXIT).

Il y'a aussi d'autre modules qui ne sont pas �gurées dans la �gure 4.1comme :

� primitives.c qui contient la dé�nition des primitives implémentéesen C, par exemple la primitive FATBAR représentée par la fonctionprimFatbar().

� output.c qui contient quelques fonctions d'écriture des programmesreprésentés en mémoire, des expressions, et des types. Ces fonctionsont été d'une grande utilité durant le déboggage du système.

Page 108: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 101

4.3.1 Types et structures de donnée utilisésUn des plus important composants du système est la gestion de la mé-

moire implémentée dans mem.c et mem.h, où il y'a la dé�nition des structuresde données utilisées dans les autres parties du système. Les structures dedonnée les plus importantes sont :

Le type de donnée CellLa gestion de la mémoire dans notre implémentation est automatique,

c'est à dire que le système inclut l'utilisation d'un grand segment de mé-moire géré par un ramassage de miettes et qui comporte un grand nombred'unités atomiques appelées cellules (Cell) (section 3.4). Ces cellules sont nonseulement utilisés dans l'exécution des programmes, mais aussi dans tout lescomposants du système, y compris dans la partie ou il y'a la constructionde l'arbre syntaxique abstrait des programmes, et aussi dans les di�érentesparties de translation.

Les représentations les plus importantes que peuvent prendre les cellulessont :Les constantes : Le type de donnée Cell inclut la représentation de plu-

sieurs constantes, tels que les entiers, les réels, les caractères et leschaînes de caractères.

Les paires : Utilisées pour représenter les valeurs composées, tel que lespaires, les arbres, les listes, elles sont aussi utilisées pour représenterles noeuds d'application durant la compilation et l'exécution des pro-grammes, les paires se composent en deux parties, qui sont des pointeursvers d'autres cellules.

Les variables : Les cellules de type variable représentent les variables, ellecomportent uniquement un pointeur vers une chaîne de caractères re-présentant le nom de la variable.

Les uples : Qui se di�érencient par le nombre de leurs composantes.Les noms : Utilisés pour représenter les valeurs et les expressions nommées,

incluant les valeurs dé�nies par l'utilisateur, les fonctions constructeurs,les primitives, et les supercombinateurs. Ces cellules contiennent unpointeur vers une structure de donnée nommée Name qui comporte leschamps suivants :� text : Qui donne le nom de la valeur représentée.� arity : Le nombre d'argument que la fonction doit attendre.� type : Le type de la valeur, s'il existe.� defn : Utilisé pour di�érencier entre les fonctions constructeurs etles fonction normales.

Page 109: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 102

� code : L'adresse du début du code généré pour le supercombinateurassocié.

� primDef : Un pointeur vers la fonction primitive associé, implémentéeen C.

Et des valeurs spéciales telle que LAMBDA ou LETREC pour indiquer quelest le type de l'expression.

Le type de donnée ModuleL'application est capable de charger plusieurs �chiers scripts, chaque

script chargé en mémoire est associé a une structure de donnée de type Mo-dule qui comporte quelque informations concernant le script.

La pile mémoirePlusieurs composants su système font appel a une pile de cellules :� L'analyseur syntaxique utilise la pile pour enregistrer des valeurs in-

termédiaires correspondantes aux fragments des programmes analysés(ces fragments seront protégé contre un éventuel balayage dans le cas oule ramasseur de miette se déclenche automatiquement en plein milieud'analyse syntaxique et lexicale).

� L'algorithme de calcul des composantes fortement connexes dans legraphe des dépendances durant l'analyse statique utilise la pile pourenregistrer les noeuds dans un graphe qui ont déjà été visités.

� L'évaluateur de la machine abstraite utilise la pile pour stocker lesvaleurs intermédiaires et les arguments des fonctions durant l'exécutiond'un programme.

Toute ces parties utilisent la même pile du système.

4.3.2 Analyse lexicale et syntaxiqueLe développement de cette partie du système est basé sur les techniques

standards de la construction des compilateurs : l'utilisation d'un analyseursyntaxique LALR généré automatiquement sous forme de fonction nomméeyyparse(), a partir d'une grammaire augmentée par des fonctions séman-tiques exprimée en C.

Nous avons utilisé le générateur d'analyseurs syntaxiques BISON [Joh78]qui est une version amélioré de YACC 1. Quand à l'analyseur lexicale, il aété implémenté manuellement et non pas généré automatiquement par l'ou-

1YACC = Yet Another Compiler Compiler, qui est un générateur d'analyseurs syn-taxiques a partir de grammaires en notation BNF

Page 110: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 103

til LEX 2 à cause de la règle de disposition (section 4.3.2) utilisée dans leslangages fonctionnels.

i. Grammaire du langageLa grammaire des programmes et des expressions du langage est dé�nie

dans le �chier parser.y, utilisé comme �chier d'entrée au programme BISONpour produire un �chier de sortie nommé parser.c qui est inclut comme unepartie de input.c.

Puisque, un de nos objectifs �xé était d'atteindre une compatibilité avecle langage Haskell, la grammaire implémentée dans notre langage représenteun sous ensemble de celle du langage Haskell, avec une légère modi�cation dequelques règles, par exemple les patterns sont analysés comme étant des ex-pressions. Ceci nous évite des con�its et des ambiguïtés non nécessaires dansla grammaire. La grammaire est représentée dans l'annexe. Après l'analysesyntaxique il y'a une analyse statique pour véri�er si ces expressions sont despatterns valides ou non.

Un autre point est à préciser, c'est le traitement des opérateurs in�xes.La grammaire du langage Haskell [Pey99] dé�nie la syntaxe des opérateursin�xes en utilisant un ensemble de règles de productions complexes indexéspar des valeurs représentant la priorité et l'associativité. Sachant que l'asso-ciativité d'un opérateur peut être gauche, droite ou indé�nie et la priorité estlimitée entre 0 et 9, on doit représenter un ensemble de 30 règles de produc-tions seulement pour les opérateurs. Implémenter ces règles directement enYACC revient à augmenter la taille de la grammaire ; Une solution consisteà lire les expressions incluants les opérateurs in�xes comme une séquence desous-expressions séparées par des symboles d'opérateurs. Quand la lecturede la séquence est terminée elle est passée a une fonction qui a pour tachede déterminer l'interprétation correcte de ces expressions en utilisant unesimple analyse de décalage/réduction (shift/reduce) suivant la priorité desopérateurs.

ii. La règle de disposition (Layout rule)Pour donner au programmeur une facilité d'expression et de lecture des

programmes, le langage utilise l'indentation pour indiquer la structure desprogrammes, par exemple la dé�nition suivante :

f x y = g (x + w)where g u = u + v

where v = u * uw = 2 + y

2LEX est un générateur d'analyseurs lexicaux

Page 111: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 104

6? 6?

- - -

règle de disposition

programmeen entrée

analyseurlexicale

analyseursyntaxique

Fig. 4.2 � la règle de disposition

montre clairement d'après l'indentation que la dé�nition de w est localeà f et non à g.

Un autre exemple où la disposition des programmes joue un rôle impor-tant est dans les deux dé�nitions suivantes, qui sont complètement di�é-rentes :

exemple x y z = a + b exemple x y z = a + bwhere a = f x y where a = f x

b = g z y b = g z

Il y'a trois cas ou l'indentation est utilisée pour déterminer la structuredes programmes :

� dans les dé�nitions globales dans un script� dans un groupe de déclarations se situant après le mot clé where oulet

� dans un groupe d'alternatives contenu dans une expression case aprèsle mot clé of

La technique de l'indentation à été utilisé pour la première fois dans lafamille de langages ISWIM de Landin il y'a plus d'un quart de siècle.

L'analyseur syntaxique LALR ne peut pas reconnaître tout seul la structured'un programme qui utilise l'indentation dans ses expressions. Pour cela ona implémenté une certaine règle appelée règle de disposition (Layout Rule[Pey99] [Jon91]) qui agit au niveau de l'analyseurs lexicale et syntaxique(�gure 4.2)et qui consiste a insérer automatiquement une des entités lexicales('{' , '}' , ' ;') dans les programmes pour enlever l'ambiguïté, en délimitantles groupes de dé�nitions et les expressions.

Par exemple la première dé�nition sera traitée comme si le programmeuravait écrit :

Page 112: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 105

f x y = g (x + w)where {g u = u + v

where {v = u * u};w = 2 + y

}Les entités lexicales '{' ,'}' et ' ;' (appelées tokens) rendent le programme

compréhensible par l'analyseur syntaxique.

La règle de disposition qui est la même utilisée par le langage Haskell,peut être décrite ainsi :

� La règle commence à agir là où le programmeur n'a pas inséré uneaccolade d'ouverture ('{') après les mots clés let, where et of, et ceci,en l'insérant automatiquement.

� L'entité lexicale ' ;' est insérée avant la première entité lexicale dechaque ligne qui possède la même indentation que la première entitélexicale se situant juste après l'accolade d'ouverture insérée ('{').

� La règle se termine et une accolade de fermeture ('}') est insérée avantla première entité lexicale d'une ligne qui possède une indentation in-férieure à celle de la première entité lexicale se situant juste après l'ac-colade d'ouverture insérée ('{').

� Les lignes contenant des espaces blancs (ou tabulation) et des commen-taires ne sont pas a�ectés par la règle de disposition.

� Pour déterminer l'indentation de chaque ligne, les tabulations sont équi-valentes à 8 espaces blancs.

� L'indentation du caractère �n de �chier (EOF) est équivalente à zéro.

4.3.3 Analyse statiqueLe but principal de l'analyse statique est de véri�er si les programmes

analysés et passés par l'analyseur syntaxique sont totalement corrects avantqu'ils ne subissent les translations nécessaires. Par exemple, prenant la dé�-nition de type structuré de la forme :data T a b c = C | D t1 t2 | E t3

où t1, t2 et t3 sont des expressions représentant des types, pour assurerque la dé�nition est valide, on a besoin de faire les tests suivants :

� véri�er s'il n y'a pas une dé�nition précédente de T.

Page 113: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 106

� véri�er le format du coté gauche de la dé�nition, c'est à dire : assurerque les arguments de T ne sont que des types variables et véri�er s'il ny'a pas de variables répétées.

� assurer que les expressions du coté droit de la dé�nition sont bien for-mées, en particulier, on a besoin d'assurer que :1. les types variables utilisées sont seulement ceux qui paraissent

dans le coté gauche de la dé�nition.2. tous les constructeurs de types référencés dans le coté droit de la

dé�nition sont dé�nis quelque part dans le programme courant.3. tous les types qui paraissent dans le coté droit de la dé�nition

sont biens formés, par exemple, le nombre d'arguments n'est passupérieur à l'arité des constructeurs de type.

� ajouter un nouveau nom de fonction pour les constructeurs C, D et Eavec leurs types et arités, ces fonction seront dé�nies comme étant desfonctions spéciales (fonctions constructeurs). En plus on a besoin devéri�er s'il n y a pas de dé�nitions précédentes de ces fonctions.

Les dé�nitions des fonctions, des primitives et des opérateurs sont ellesaussi passées à l'analyseur statique pour des tests de nature similaire. Lavéri�cation de ces conditions pour les programmes facilite la détection deserreurs, en plus elle permet de simpli�er le code pour les étapes suivantes detranslation et de compilation.

L'analyseur statique n'a pas été incorporé dans l'analyseur syntaxiquepour permettre au programmeur de mettre les dé�nitions dans un ordrequelconque, pour cela, les véri�cations statiques ne sont lancées que quandle programme a été entièrement lu.

4.3.4 Gestion de la mémoireDans les langages fonctionnels modernes, la gestion de la mémoire est

automatique, libérant ainsi le programmeur d'une lourde tâche. En e�et,l'allocation et la libération des cellules de mémoire est automatique, pourpermettre un fonctionnement durable du système sans épuisement de la mé-moire.

La gestion de la mémoire se fait par un composant nommé ramasseur demiettes (garbage collector), qui est implémenté de la façon suivante :

1. Le ramasseur de miettes identiti�e quelles sont les parties du segmentde mémoire qui sont réutilisables en les reliant dans une liste de celluleslibres, disponible à toute demande, durant toutes les étapes d'analyse,de compilation et d'exécution, appelée freeList. Une cellule est alorsallouée en enlevant un élément de la liste freeList.

Page 114: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 107

HHHHHHHHj

?-

(c)(b)

(a)

(c)(b)

(a) Avant le ramassage des mièttes

Après le ramassage des mièttes

(Miette) Ind Un autre noeud

Ap

Ap

Un autre noeudInd

Fig. 4.3 � Élimination des cellules d'indirection durant le ramassage desmiettes

2. Quand la liste des cellules libres devient vide, le ramasseur de miettesest appelé une autre fois pour construire une nouvelle liste à partir descellules inutilisées, donnant ainsi l'impression qu'il y'a une in�nité decellules en mémoire.

La construction de la nouvelle liste se fait en utilisant la technique "marquage-balayage" [Pey87] [Jon94] dont les caractéristiques sont les suivantes :le marquage se fait sur toutes les parties du système qui sont encore en

utilisation, telles que les pointeurs vers les structures de données quidoivent être épargnés pour éviter un éventuel écrasement, et les racinesdes dé�nitions globales. En plus des cellules pointées par la pile dusystème.Le marquage se fait par la fonction mark() grâce à un message MARKpassé par l'appel de la fonction everybody(MARK) aux fonctions decontrôles des di�érentes parties du système (section 4.3) a�n de lancerun parcours récursif à travers les graphes des données locales de chaquepartie. Durant ce parcours récursif les cellules d'indirection sont élimi-nées (�gure 4.3)

le balayage se fait directement dans le segment de mémoire contenant lescellules, par l'insertion de chaque cellule non marqué au sommet de laliste des cellules libres, ensuite, le démarquage des cellules marquées (etdonc préservées) pour un prochain ramassage de miettes. le balayagese lance par l'appel de la fonction garbageCollect().

Page 115: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 108

4.3.5 Compilation en supercombinateursLa translation du programme en un ensemble de supercombinateurs est

implémentée dans le module compiler.c, elle est divisée en deux parties :1. Une transformation du programme en langage plus simple, en suppri-

mant le pattern matching, les listes de compréhension et les cases. Undes avantages de cette transformation est l'enlèvement de la complexitépour le prochain stade de la compilation.La transformation se fait par un ensemble de parcours récursifs à tra-vers le graphe représentant le programme, appliquant des modi�cationslocales à celui-ci. Durant cette transformation il y a une création denouvelles variables avec des noms uniques, ces noms sont générés parla fonction inventText() sous forme de chaîne contenant un caractère'v' suivi par un nombre, par ex. "v49".

2. Un lambda lifting du programme résultant de la première partie, im-plémenté d'après l'algorithme déjà présenté [Joh85].

4.3.6 Implémentation des primitivesDurant l'évaluation, le système a besoin d'appeler les fonctions primitives

implémentées directement en C, à chaque primitive est donné un nom globalattribué, contenant l'adresse de la fonction associée dans le champ pointeurprimDef.

par exemple la primitive SEL-t-n est représentée par le nom nameSel dontle champs text est _SEL. Les fonctions telles que head et tail deviennentalors :

head u = _SEL (:) u 1tail u = _SEL (:) u 2

4.3.7 Instructions de la machine abstraiteOn a mentionné que les primitives de notre système sont implémentées

directement en C. Toutes les autre fonctions, écrites en langage fonctionnel,y compris celles implémentées dans notre script de bibliothèque standardstdlib.hlg (section 4.4)sont représentées par une séquence d'instructions deG-code stockées dans un espace contigu de mémoire, et qui seront exécutéespar la machine abstraite de l'évaluateur, ces instructions sont divisées endeux groupes : des instructions de constructions de graphes, et d'autrede contrôle de l'évaluation.

Page 116: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 109

Instructions de constructionsLes instructions suivantes sont utilisées pour la construction des fragments

de graphes qui représentes les programmes :� PUSH n : Met au sommet de la pile l'élément qui existe déjà dans la pile

à la position n du sommet. Cette instruction est utilisée pour accéderà la valeur des arguments d'une fonction ou des variables localementdé�nies.

� PUSHGLOBAL c : Met au sommet de la pile l'élément dé�ni globalementc, tel que les supercombinateurs et les fonctions constructeurs.

� PUSHCHAR n : Met au sommet de la pile un caractère de la valeur en-tière équivalente n.

� PUSHINT n : Met au sommet de la pile l'entier de valeur n.

� PUSHFLOAT f : Met un �ottant de valeur f au sommet de la pile.

� PUSHSTRING str : Met un pointeur vers une chaîne de caractères devaleur str au sommet de la pile.

� MKAP n : Construit une application de fonction dont le pointeur est ausommet de la pile, à un ensemble d'arguments se situant dans les ncases avant ce sommet. Ces n + 1 éléments dans la pile sont rempla-cés par l'expression résultante. Notons que cette instruction n'impliqueaucune évaluation, ni de la fonction, ni de ces argument, ni même del'expression résultante.

� UPDATE n : Enlève l'élément, dit r, se trouvant au sommet de la pile,et remplace les n éléments avant ce sommet par un pointeur vers unecellule d'indirection de r. Cette instruction est utilisée pour écraser laracine d'une expression, pour assurer un partage de la valeur de cetteexpression (seconde notion de l'évaluation souple). Elle est aussi utili-sée pour stocker la valeur des variables localement dé�nies.

� UPDAP n : Cette instruction à les mêmes e�ets qu'une instruction MKAP 1suivie d'une autre instruction UPDATE n ; en d'autres mots, les n élé-ments se trouvant au sommet de la pile sont remplacés par une ap-plication formée à partir des deux éléments du sommet de la pile. Ladi�érence est que cette instruction est utilisée quand on connaît quele n-ième élément du sommet de la pile pointe vers un noeud d'appli-

Page 117: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 110

cation ; dans ce cas l'ancien noeud d'application est remplacé par unenouvelle valeur, évitant d'allouer de nouveaux noeuds d'application.

� ALLOC n : Alloue n cellules avec une valeur initiale NIL, et empile unpointeur vers chaque cellule allouée. Cette instruction fait partie duprocessus d'initialisation des variables locales récursives.

� SLIDE n : Glisse le pointeur se trouvant au sommet de la pile de n casesà l'intérieur de la pile en e�açant ces n cases. Cette instruction est utili-sée pour fonctionner avec les expressions incluant des dé�nitions locales.

Instructions de contrôleLes instructions suivantes sont utilisées pour appeler l'évaluateur, à tes-

ter des valeurs, indiquer la �n d'une réduction ou signaler une expressionirréductible.

� EVAL : Dépile le sommet de la pile et appelle l'évaluateur pour calcu-ler sa valeur. Cette instruction est souvent utilisée pour les fonctionsstrictes.

� JUMP addr : Cause un branchement inconditionnel à l'adresse addr.

� RETURN : Signale la �n d'un supercombinateur. Cette instruction rem-place l'instruction UNWIND qui a presque les même e�ets, sauf queUNWIND demande au système de trouver le prochain redex (cette tâcheest déjà implémentée dans notre évaluateur lui même).

� FAIL : Elle est utilisée pour signaler un échec de pattern matching enretournant une erreur d'exécution.

4.4 le �chier de la bibliothèque standard "std-lib"

Les dé�nition des fonctions standards telles que map ou foldr sont sto-ckées dans un �chier script nommé stdlib.hlg . Ce �chier est chargé auto-matiquement à chaque lancement du programme, rendant disponible toutesles fonctions standards.

Page 118: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 111

-�

-�

-�

6UtilisateurInterfaceutilisateur

Compilateur

Evaluateur

Fig. 4.4 � interface avec l'utilisateur

4.5 Interface utilisateurLe but de notre application est de fournir les deux tâches essentielles :1. L'analyse totale avec une gestion des erreurs de programmation, la

compilation et l'exécution des programmes.2. La facilité de programmation avec la fourniture d'outils d'aide à la

programmation.La première tâche a été décrite précédemment, avec la description interne

du processus d'analyse, de compilation et d'évaluation.La deuxième tache réside dans l'interface utilisateur.Notre interface a été implémentée pour être lancée dans un processus

séparé qui s'exécute en parallèle avec le processus d'analyse, de compilationet d'évaluation (�gure 4.4). L'objectif ici est de permettre la continuité dudéveloppement de chaque partie du programme en évitant les complicationsconcernant les autres parties.

L'interface à été implémentée en utilisant la bibliothèque graphique QT/KDEdistribué librement par trolltech 3, elle est composée de quatre parties di�é-rentes :

1. Un éditeur de texte qui utilise la coloration syntaxique pour donner plusde clartés au programmes écrits. cet éditeur possède les fonctionnalitésessentielles d'un éditeur de texte avancé (recherche et remplacement detexte, sélections et utilisation du presse papier, indentation ...etc ).

2. Une partie qui sert de pont entre l'utilisateur et le compilateur/évaluateur,grâce à l'interprétation des commandes lancée par l'utilisateur et la re-direction de ces commandes vers le compilateur/évaluateur.

3. Une partie qui contient la liste de toutes les dé�nitions compilées etchargées en mémoire.

3le site o�ciel de trolltech est http ://www.trolltech.com

Page 119: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 112

4. Une partie qui montre la trace de l'exécution grâce a l'a�chage du codegénéré lors de la compilation des programmes.

La �gure 4.5 montre l'interface avec un exemple d'exécution de la fonctiondu tri rapide d'une liste d'entiers, ainsi que son G-code.

Page 120: Compilateur orienté lambda calcul

CHAPITRE 4. IMPLÉMENTATION DU COMPILATEUR 113

Fig. 4.5 � Interface graphique avec exemple et son code généré

Page 121: Compilateur orienté lambda calcul

Conclusion générale

114

Page 122: Compilateur orienté lambda calcul

Annexe A

Règles d'écriture du langage

Dans cette section on trouve la grammaire du langage utilisé par le com-pilateur. Les non-terminaux `exp` et `script` représentent respectivement lasyntaxe des expressions entrées a l'évaluateur, et celle des dé�nitions conte-nues dans les scripts.

Les notations suivantes sont utilisées dans la grammaire qui est écrite ennotation bnf :

� les bars `|', pour séparer les alternatives.� { et } pour exprimer les répétitions� [ et ] pour les éléments optionnels� ( et ) pour enlever l'ambiguïté� un terminal est noté : terminal� les autres représentent les non-terminaux

Les terminaux suivants sont utilisés dans la grammaire :varid : identi�cateur commençant par minusculeconid : comme varid, mais commence avec un majusculevarop : opérateur qui est une combinaison de: ! # $ % & * + . / < = > ? @ \ ^ | -

int : entier constant�oat : réel constantchar : caractère constantstring : chaine de caractères

115

Page 123: Compilateur orienté lambda calcul

ANNEXE A. RÈGLES D'ÉCRITURE DU LANGAGE 116

Entête de la grammairescript : := { topdecls } scriptexp : := exp [where] expression à evaluertopdecls : := topdecls ; topdecls déclarations

| data typeLhs = constrs de type structuré| type typeLhs = type synonyme de type| infixl [digit] op { , op} opérateurs in�xes| infixr [digit] op { , op}| infix [digit] op { , op}| primitive prims : : type primitives internes| decls déclaration de fonctions

typeLhs : := conid {varid}constrs : := constrs | constrs

| conid {type}prims : := prims , prims

| var string

Expression du typetype : := ctype [ -> type ]ctype : := conid {atype}

| atypeatype : := varid

| ()| ( type )| ( type , type { , type} )| [ type ]

Page 124: Compilateur orienté lambda calcul

ANNEXE A. RÈGLES D'ÉCRITURE DU LANGAGE 117

Déclarations de valeursdecls : := decls ; decls

| var { , var} : : type| fun rhs [where]

rhs : := = exp| gdRhs {gdRhs}

gdRhs : := = exp, if exp| = exp, exp

where : := where { decls }fun : := var

| pat varop pat| ( pat varop )| ( varop pat )| fun apat| ( fun )

Page 125: Compilateur orienté lambda calcul

ANNEXE A. RÈGLES D'ÉCRITURE DU LANGAGE 118

Expressionsexp : := \ apat {apat} -> exp

| let { decls } in exp| if exp then exp else exp| case exp of { alts }| opExp : : type| opExp

opExp : := opExp op opExp| pfxExp

pfxExp : := - appExp| appExp

appExp : := appExp atomic| atomic

atomic : := var| conid| int| �oat| char| string| ()| ( exp )| ( exp op )| ( op exp )| [ list ]| ( exp , exp { , exp} )

list : := [ exp { , exp} ]| exp | quals| exp ..| exp , exp ..| exp .. exp| exp , exp .. exp

quals : := quals , quals| pat <- exp| exp

alts : := alts ; alts| pat altRhs

altRhs : := -> exp

Page 126: Compilateur orienté lambda calcul

ANNEXE A. RÈGLES D'ÉCRITURE DU LANGAGE 119

Patternspat : := appPatappPat : := appPat apat

| apatapat : := var

| conid| int| char| string| ()| ( pat )| [ [ pat { , pat} ] ]| ( pat , pat { , pat} )

Variables et opérateursvar : := varid

| (-)op : := varop

| -varid : := varid

| ( varop )varop : := varop

| ` varid `conid : := conid

Page 127: Compilateur orienté lambda calcul

Bibliographie

[Bar84] H. P. Barendregt (1984) The Lambda Calculus - Its Syntax and Se-mantics. 2nd edition. North-Holland

[Bro94] F. B. Brokken, Karel Kubat (1994) C++ Annotations. ICCE, Uni-versity of Groningen.

[Chau89] J-R Chauvière (1989) Developper sous UNIX : outils pour la pro-duction de logiciels . editests

[Del94] C. Delannoy (1994) Programmer en Langage C++. CHIHAB-EYROLLES.

[Hao99] A. Haouas (1999) Programmation fonctionelle : Concetps, Principes,Lambda Calcul, Implementation.

[Hor88] T.Horn (1988) Lisp. 2nd edition. Addisson Wesley[Hud94] P. Hudak, Mark P. Jones (1994) Haskell vs. Ada vs. C++ vs. Awk vs.

... An experiment in Software Prototyping Productivity. Yale University,Department of Computer Science.

[Hug90] J. Hughes (1990)Why Functionnal Programming Matters. ChalmersUniversity , Department of Computer Science.

[Joh78] S. C. Johnson (1978) YACC : Yet Another Compiler-Compiler. Unixprogrammers manual.

[Joh85] T. Johnsson (1985) Lambda Lifting : Transforming Programs to Re-cursive Equations. Proceedings 1985 Conférence on functionnal pro-gramming languages and computer architecture.

[Jon91] M. P. Jones (1991) An Introduction to Gofer. librement disponniblepar ftp au site nebule.cs.yale.edu, repertoire pub/haskell/gofer

[Jon94] M. P. Jones (1994) The Implementation of The Gofer Functio-nal Programming System. Yale University, Department of ComputerScience.

[Lon89] J. Lonchamp (1989) Langages de Programmation : Concepts, Evo-lution, Classi�cation. Masson.

120

Page 128: Compilateur orienté lambda calcul

BIBLIOGRAPHIE 121

[Nol99] Ralf Nolden (1999) The KDevelop Programming Handbook : TheUser Guide to C++ Application Design for the K Desktop Environ-ment (KDE) with the KDevelop IDE, Version 1.0 . The KDevelop Team(http ://www.kdevelop.org)

[Pey87] S. L. Peyton Jones, (1987) The Implementation of Functional Pro-gramming Languages. Prentice Hall International Series in ComputerScience. Prentice Hall, Hemel Hempstead.

[Pey91] S. L. Peyton Jones (1991) Implementing Functional Languages : atutorial. University of Glasgow, Department of Computer Science.

[Pey93] S.L. Peyton Jones, C. Hall, K. Hammond, W. Partain, P. Wadler(1993) The Glasgow Haskell Compiler : a retrospective. In Proceedings ofthe 1992 Glasgow workshop on functionnal programming, Ayr, ScotlandUniversity of Glasgow, Department of Computer Science.

[Pey99] S. L. Peyton Jones,J. Hughes (1999) report on Haskell 98 : a nonstrict, purely functionnal language. University of Glasgow, Departmentof Computer Science.

[Tur86] D. Turner (1986) An Overview of Miranda. University of Kent ,Computing Laboratory.

[Gla90] H. Glaser, C. Hankin, D. Till (1987) Principes de programmationfonctionelle. Masson.

Une bonne partie des documents référencés ainsi que les sources du me-moire en LATEX, sont disponnibles et gratuitement téléchargeable sur le sitehttp://hakim.programmeurs.net. Pour tout renseignement, contacterhakim Feghoul sur [email protected]