Université de la Méditerranée

Ecole Supérieure d’Ingénieurs de Luminy

Etudes Supérieures en Ingénierie Informatique

 

Projet de compilation

_______________

Compilateur P2Mips

_______________

 

Enseignants responsables :

Marc GENGLER

Henri KANOUI

Laurent HENOCQUE

 

 

 

 

Stéphane BENOLIEL

Virginie COLLOMB

Philippe ELDIN Promotion 1999

Guillaume LEVASSEUR Mars 1998

Thomas PEYRIC

Copyright©

Sommaire

1.Présentation *

1.1.Le sujet *

1.2.Le planning *

2.analyse lexicale *

2.1.Transformation avec LEX *

2.2.La classe Utilitaire *

2.2.1.Les fonctions de gestion de flux *

2.2.2.Les globales *

2.2.3.La fonction de DEBUG *

2.2.4.La gestion des erreurs *

3.Syntaxique & sémantique *

3.1.La structure des classes *

3.2.Schéma des classes *

3.3.La table des symboles *

3.4.La classe Nœud *

3.4.1.Représentation des nœuds de l’arbre *

3.4.2.Construction et insertion d’un nœud dans l’arbre *

3.5.La génération de l’arbre *

3.5.1.Programme *

3.5.2.SuperBloc *

3.5.3.Bloc *

3.5.4.Fonction & procédure *

3.5.5.Instructions *

3.6.Conversion de type *

3.6.1.Recherche du type prioritaire *

3.6.2.Insertion des nœuds de conversion et modification du type des opérateurs *

3.7.Les listes chainées *

4.Génération du code intermédiare *

4.1.Choix du code généré *

4.2.Principes de génération du code *

4.3.Les variables globales et locales *

4.4.Procédures, Fonctions et récursivité *

4.5.Génération des labels *

4.6.Codage d’un flottant sur Mips *

5.Un exemple de A à P... *

5.1.Le fichier Pascal *

5.2.Passe 1 : Lex *

5.3.Passe 2 : Syntaxique & Sémantique *

5.4.Passe 3 : Le code *

6.Documentation de l’utilisateur de P2Mips *

6.1.La structure de base *

6.2.Les déclarations *

6.3.Les boucles *

6.4.Les opérations *

6.5.Les procédures fonctions *

6.6.Les entrées/sorties *

7.Bilan *

Sources *

1.Présentation

1.1.Le sujet

 

Faire un compilateur est une étape importante de notre cursus. Ce projet nous a été proposé pour nous permettre de mettre en application les connaissances acquises dans le domaine de la compilation, de la théorie des langages et de la programmation orientée objets.

Le sujet était de réaliser un compilateur de langage Pascal générant un code exécutable sur une machine à pile de notre choix. Notre première orientation a été la Java Virtual Machine qui utilise ce concept, mais des problèmes liés à la protection du P-code Java nous a fait changer d’orientation. Notre choix définitif a donc été d’utiliser une machine réelle, l’assembleur Mips. Lors d’un précédent projet nous avions utilisé un simulateur pour ce type d’architecture. Cette optique nous a permis d’entrevoir une phase de compilation plus proche de la réalité.

Après une analyse précise du sujet, nous avons défini, les différentes phases de l’élaboration de notre compilateur. Nous pouvons différencier quatre grandes étapes :

? L’analyse lexicale traduit une suite de caractères en lexèmes.

? L’analyse syntaxique effectue une concordance entre les règles grammaticales liées au langage, et le programme.

? L’analyse sémantique réalise une vérification du sens du programme.

? La génération du code est la dernière étape. Elle convertie le programme correct en code exécutable.

La seconde partie de notre analyse nous a permis de définir une répartition des taches dans le temps, adéquate et efficace. Le planning de la page suivante modélise cette idée.

1.2.Le planning

 

DIAGRAMME DE GANTT

Université de la Méditerranée

Ecole Supérieure d’Ingénieurs de Luminy

Etudes Supérieures en Ingénierie Informatique

 

PROJET : COMPILATION

Début : 01.02.1998

Fin : 24.03.1998

Tâches :

5

6

7

8

9

10

11

12

13

 

 

 

 

 

 

 

 

 

 

 

 

 

Analyse

P

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

préliminaire

R

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Analyse

P

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

lexicale

R

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Génération de

P

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

l’arbre

R

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Analyse

P

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

sémantique

R

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Génération du

P

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

code SPIM

R

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Rédaction du

P

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

rapport

R

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Etabli par : Stéphane BENOLIEL, Virginie COLLOMB, Philippe ELDIN, Guillaume LEVASSEUR et Thomas PEYRIC

2.analyse lexicale

La première étape d’un compilateur est l’analyse lexicale. En effet il faut traduire et analyser le programme pour vérifier l’emploie exclusif du vocabulaire de la grammaire. Nous avons dans notre cas utilisé un outils intéressant pour cette étape, lex.

2.1.Transformation avec LEX

Lex est un analyseur syntaxique fourni en standard sous UNIX, sous la forme d’une bibliothèque C. Il permet de transformer un flux d’entrée, de l’analyser et d’effectuer les opérations désirées sur le vocabulaire. Nous avons utilisé cette bibliothèque pour simplifier la tâche des analyses suivantes.

Nous avons transformé tous les éléments du vocabulaire, en code, ce qui permet à des fonctions d’analyser simplement les termes employés. Le code utilise deux concepts, les chaînes prédéfinies et les autres. La première catégorie peut être simplement codée par un numéro. En effet si nous codons le mot clé if par le numéro 021, on aura bien un code efficace. Par contre coder une chaîne de caractère ne peut être effectué de la sorte. Nous avons donc introduit un second code. Nous utilisons pour les chaînes, les identificateurs, les entiers et les réels, un autre code ; un code numérique suivi de la chaîne correspondant à l’objet. D’autre part pour des questions de commodité nous avons utilisé des séparateurs, le symbole $.

En résumé nous avons transformé un fichier Pascal en fichier code :

program monprog ;

var mavar : integer ;

begin

end.

$037 $090 monprog$ $084

$052 $090 mavar$ $059 $22 $084

$005

$017 $073

Cette transformation assure de plus, que tous les éléments du fichier Pascal, correspondent bien à un mot du vocabulaire. Si ce n’est pas le cas, l’analyse lexicale interrompt la compilation.

Pour faciliter l’utilisation d’un tel code pendant la phase de développement, nous avons créé un ENUM qui convertit les codes numériques en codes littéraux.

Ce format de codage a été défini pour faciliter la lecture du code lors des analyses suivantes. Ceci a été réalisé à l’aide de fonctions que nous avons implantées dans la classe Utilitaire.

 

2.2.La classe Utilitaire

La classe utilitaire regroupe toutes les fonctions de gestions de flux, la macro Assert, la fonction de débuggage, la numérotation de la ligne courante et le numéro du dernier objet créé.

 

2.2.1.Les fonctions de gestion de flux

Elles sont au nombre de six divisées en deux groupes de trois, celles qui prennent des informations dans le flux standard et celles qui se contentent de lire le flux. Il existe trois sortes de données sur le flux : les " code " les " var " les " chaîne ".

Nous avons sur le flux des codes $xxx représentant la conversion de symbole par LEX, les données var et chaînes sont précédées d’un code particulier.

Soit i le flux d’entrée standard : $090 pipo$ $084

code=global.PrendreCode(i) ; Le flux devient : pipo$ $084 et code=90

global.PrendreVar(i,variable); Le flux devient : $084 et variable=pipo

Soit i le flux d’entrée standard : 'l''essai est probant'$ $084

global.PrendreChaine(i,chaine); Le flux devient $084 et chaine=l’essai est probant

Les fonctions de lecture sont identiques le pointeur de début de flux est sauvegardé puis restitué à la fin de l’opération de lecture. Nous utilisons pour cela les fonctions seekg et tellg.

Deux autres fonctions de flux sont implantées : l’une convertie une chaine ou un flux en réel flottant et l’autre permet la lecture d’une variable préfixée de son code.

Soit i le flux d’entrée standard : $090 pipo$ $084

global.LireCodeVar(i,variable) ; Le flux reste inchangé et variable=pipo

Soit i le flux d’entrée standard : 53.65e-3$ $084

res=global.Char2Double(i); Le flux devient : $084 et res=0.5365E-1

 

2.2.2.Les globales

Il y a deux variables entières globales, l’une permet de connaître à tout instant la ligne du programme source en cours de traitement et l’autre permet de connaître le nombre d’objets référencés dans les différentes tables des symboles. C’est une CLEF PRIMAIRE permettant de retrouver n’importe quels objets.

 

2.2.3.La fonction de DEBUG

Nous avons implanté une gestion de débugage qui lorsque la variable globale _debug est activée produit un affichage, plusieurs niveaux de debug peuvent être implantés.

 

2.2.4.La gestion des erreurs

Il s’agit d’une gestion très sommaire pour relever les exceptions, en effet, il ne s’agit que de la redéfinition de la macro assert ; c’est une gestion bloquante. Un numéro d’exception est adjoint a la fonction assert permettant un affichage de la ligne ou s’est déclenchée l’erreur, le numéro du dernier objet créé ainsi que la nature de l’erreur.

 

3.Syntaxique & sémantique

L’étape suivante est composée de la vérification syntaxique et sémantique qui doivent aboutir à la génération de l’arbre.

3.1.La structure des classes

La première étape de notre réflexion a été d’établir une structure de programmation nous permettant de faciliter la création de l’arbre pendant la vérification de la syntaxe. Nous avons donc défini une structure basée sur une classe principale, la classe Nœud. Cette classe, à l’origine de la création de l’arbre, sera expliquée ultérieurement.

Notre raisonnement a commencé par une réflexion sur la structuration du langage Pascal. Ce dernier est construit en deux grandes parties. Une partie déclarative et une partie de définitions.

La classe de départ est un Bloc. Elle hérite naturellement de Nœud, pour permettre la création de l’arbre. Cette classe génère un objet qui est la structure de base de la définition du code. Elle possède une table des symboles pour permettre la visibilité des variables, fonctions et autres déclarations.

La classe SuperBloc est une sous classe de la classe Bloc. Elle implémente le concept de la déclaration. Nous avons donc le principe implanté dans le schéma. Il en est de même pour la classe Program, Procedure, et Function qui sont des Superbloc particuliers.

La deuxième partie des classes héritant de Bloc sont de deux types :

? Les instructions : Série de classes correspondant à un mot clé précis. Nous avons les tests et les autres. Chacune de ces classes comportent un constructeur capable de définir un traitement particulier.

? Les Nœuds particuliers : Série de nœud qui comportent des informations supplémentaires.

Le principe de base est une analyse prenant un objet istream en argument, et lors de l’appel des constructeurs des classes, la génération de l’arbre et l’analyse du code se font simultanément. Lors de la création d’un SuperBloc, nous utilisons l’initialisation d’un Bloc. Le fait que nous ayons besoin d’utiliser la construction du SuperBloc avant, nous a obligé à ne pas appeler le constructeur définie du Bloc, mais un constructeur par défaut, suivie par une fonction d’initialisation de Bloc. En effet nous voyons sur le schéma, que la partie déclarative est avant la partie de définition.

Pour permettre le bon déroulement du programme il nous a fallu une série de classes ne faisant pas partie de cette architecture (voir schéma des classes). Nous allons maintenant détailler plus précisément l’implantation de toutes ces classes.

3.2.Schéma des classes

 

3.3.La table des symboles

Lors de l’élaboration du compilateur, il nous a fallu faire une table des symboles, permettant d’identifier toutes les variables, quelles soient globales, où locales à un bloc. Notre choix a été alors d’associer une table par bloc. Cette méthode, possible grâce au langage objet comme C++, nous a permis d’éviter les problèmes de visibilité. Il est naturel, lors de l’élaboration d’un programme de considérer que la liste des variables accessibles, sont celles du bloc courant, et des blocs qui l’englobe. Nous avons repris exactement la même philosophie.

La classe TableSymbole est un objet contenant des membres, des constructeurs et des accesseurs, qui définissent une déclaration. Le bloc contient un membre _table_symbole qui est une liste chainée de ces éléments de base. Un objet TableSymbole se compose comme suit :

Référence

Nom

Type

Type de retour

Dimension

Paramètre[10]

Valeur

? La référence : C’est un numéro unique permettant d’identifier un objet. C’est une clé primaire.

? Le nom : C’est le nom utilisé par le programmeur pour appeler l’objet.

? Le Type : C’est le type de l’objet, autrement dit :

Variable, Constante, Type utilisateur, Fonction, Procédure

? Le Type de retour : C’est soit le type de retour dans le cas des fonctions, soit le type de l’objet référencé pour les variables, les constantes...

Les types connus sont : Boolean, Char, Integer, Real

? La Dimension : Lors de la déclaration d’un tableau c’est la dimension de ce dernier, lors de la déclaration d’une fonction où procédure, c’est le nombre de ses paramètres.

? Les Paramètres : C’est un tableau de 10 éléments. Pour les tableaux, ce sont les tailles respectives des dimensions. Pour les fonctions et procédures, ce sont les types de retour des différents paramètres.

? La Valeur : C’est la valeur de l’objet Const. Lors de la création d’une constante on met la valeur dans la table des symboles.

La gestion de la table des symboles se fait par l’intermédiaire de deux types de fonctions ; les AddTableSymbole et les FindTableSymbole. La première catégorie de fonctions, sont des fonctions d’ajout d’éléments dans la table des symboles. Elles vérifient que l’objet n’existe pas dans la table locale, et ajoute la référence. Il existe plusieurs types de fonctions d’ajout, permettant l’insertion de plusieurs types d’éléments. La seconde est une fonction de recherche dans la table des symboles, à partir d’un nom ou d’une référence, elle recherche dans les tables des symboles visibles l’objet. Ces fonctions sont essentielles au compilateur pour fonctionner normalement. Nous avons pour les utiliser depuis n’importe quel nœud, fait également une fonction de recherche de bloc, qui renvoie le bloc local dans lequel le nœud se trouve.

Nous avons fait un choix, à même de faciliter la compilation, en effectuant les ajouts et les vérifications dans la table des symboles lors de la création de l’arbre. Cette façon de procéder, est rendue possible grâce à la structuration de nos classes, et nous évite de créer les nœuds inéluctables aux déclarations. C’est une optimisation intéressante qui accélère la compilation.

3.4.La classe Nœud

3.4.1.Représentation des nœuds de l’arbre

Au sein de l’arbre de dérivation nous avons défini cinq types de nœuds.

Ä Le nœud de base comporte un champ type représentant une instruction ou un mot réservé du langage Pascal. Cinq pointeurs lui permettent de connaître le nœud père, le nœud situé à sa gauche et à sa droite, le nœud fils et le nœud du dernier fils.

 

 

 

 

 

 

 

 

 

 

 

Représentation du nœud de base

Le chaînage entre un nœud père et ses fils est donc représenté comme ceci :

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Chaînage entre un nœud père et ses fils

 

Ä Les nœuds " NœudInt ", " NœudBool " et " NœudReal " ont exactement la même structure que le nœud de base. Ils possèdent un champ supplémentaire qui représente la valeur numérique du nœud.

 

 

 

 

 

 

 

 

 

 

 

 

Représentation des " NœudInt ", " NœudBool " et " NœudReal "

 

Ä Le nœud " NœudVar " a la même structure que le nœud de base. Il possède un champ supplémentaire qui représente son identifiant au sein de la table des symboles associé à son bloc de données.

 

 

 

 

 

 

 

 

 

 

 

Représentation du " NœudVar "

 

3.4.2.Construction et insertion d’un nœud dans l’arbre

La construction d’un nœud nécessite de spécifier les pointeurs Père, Petit_frère, Fils et le champ (type si c’est un nœud de base, valeur numérique si c’est un " NœudInt " ou " NœudReal " ou " NœudBool ", référence si c’est un nœud " NœudVar ").

Les pointeurs Grand_frère et Petit_dernier ne sont pas accessibles à l’utilisateur et ont été implémentés pour faciliter la gestion interne de l’arbre et pour améliorer les temps de parcours.

L’insertion d’un nœud dans l’arbre peut être réalisée de deux façons. La première consiste simplement à spécifier le type du nœud à insérer. La deuxième nécessite au préalable la construction d’un nœud et se contente de l’attacher au sein de l’arbre.

Voici la description de la mise à jour des pointeurs lors d’une insertion.

Ä Insertion d’un nouveau nœud à partir d’un nœud qui n’a pas de fils :

 

 

 

 

 

 

 

 

 

 

 

 

 

Avant insertion du nœud

 

 

 

 

 

 

 

Après insertion du nœud

 

Ä Insertion d’un nouveau nœud à partir d’un nœud qui a un ou plusieurs fils :

Lorsque nous voulons insérer un nœud sous un maillon qui possède déjà des fils, nous le plaçons à la suite de tous les fils.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Avant insertion du nœud

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Après insertion du nœud

 

 

Lorsqu’un nœud a été créé et inséré, l’utilisateur ne peut en aucun cas modifier les pointeurs Père, Petit_frère, et Fils.

 

3.5.La génération de l’arbre

3.5.1.Programme

La détection du " ; " déclenche l’appel de la construction de SuperBloc et attend un " . " final.

3.5.2.SuperBloc

Il y a trois phases dans SuperBloc, une phase de détection des déclarations ( VAR, CONST, TYPE ) avec inscription des déclarations dans la table des symboles. Une 2ème phase consiste à détecter d’éventuelle définition et déclaration des fonctions et procédures. Cette phase est primordiale puisque c’est elle qui génère la table des symboles. Et finalement une phase d’implantation du code.

3.5.3.Bloc

C’est le nœud initiateur de l’implantation du code, c’est lui qui effectue la vérification de la syntaxe, des instructions et déclenche le constructeur approprié. Il réalise également un contrôle sémantique entre les identificateurs utilisés dans son bloc et ceux déclarés dans les différentes tables des symboles.

 

3.5.4.Fonction & procédure

Il y a 2 phases ; une phase de définition et une phase de déclaration. La définition permet la récursion dans des procédures ou des fonctions imbriquées ; la définition permet une première vérification sémantique. La gestion du passage des paramètres est en interaction direct avec la table des symboles.

Function pipo(a :integer ; var b :real ; tab :tableau) :integer ;

Va générer, à la déclaration, une entrée dans la table des symboles de la forme :

 

Référence

Nom

Type

Type de retour

Dimension

Paramètre[10]

? ? ?

pipo

FUNCTION

INTEGER

3

INTEGER VARREAL USERTYPE

 

Lors de la définition :

Référence

Nom

Type

Type de retour

Dimension

Paramètre[10]

? ? ?

a

VARIABLE

INTEGER

0

0

? ? ?

b

VARIABLE

VARREAL

0

N° ref

? ? ?

tab

VARIABLE

USERTYPE

0

N° ref

Automate de fonction

Automate de procédure :

 

 

 

3.5.5.Instructions

Tout langage comporte un certain nombre d’instructions de base nécessaires à toute opération. Ce sont en Pascal, le calcul des expressions, des affectations, des tests, des appels de procédures et de fonctions et nous y avons rajouté des primitives d’entrée/sortie.

Nous allons détailler l’implantation dans l’arbre des diverses instructions et règles de la grammaire.

Principe générale

Le principe de base de l’implantation de la grammaire dans l’arbre est résumé par le schéma suivant. Nous avons ici différents types de nœud d’un arbre, où a et b sont des symboles pouvant représenter différents vocabulaires.

L’aspect intéressant de ce schéma est la formation d’un nœud de type bloc, qui est le principe de base de la construction d’un programme. Un programme, est en effet une imbrication de blocs.

Expression

Une expression est une succession d’éléments qui permettent de calculer une expression arithmétique, c’est à dire un nombre.

Le but de notre travail est tout d’abord de lire cette expression, mais surtout de la représenter sous une forme facile à comprendre, à visualiser et à utiliser pour les opérations futures. C’est pour cela que nous utilisons un arbre.

Dans cet arbre chaque feuille représentera un terminal, et chaque nœud un connecteur différent ( opérateur, nom d’identificateur ).

Tous les noeuds non terminaux sont de même type ( ici Nœud ). Tous les terminaux ont quant à eux un type différent, correspondant au rôle de ces terminaux dans l’expression. Ces sous-types héritent tous de la classe Noeud, et ont chacun des éléments supplémentaires qui leur sont propres.

 

si la feuille est un entier , le noeud correspondant sera de type NoeudInt

réel NoeudReal

booléen NoeudBool

idem pour les identificateurs qui correspondent aux variables, constantes, fonctions ou bien tableau sauf qu’ils seront de type NoeudVar.

Tout ceci nous permet de gérer un arbre très efficacement qui est lui même très efficace.

 

 

 

Remarques :

Nous allons par contre voir comment sont représentés les tableaux car c’est dans cette section que nous les traitons.

Tout d’abord il faut trouver un identificateur en entrée, et vérifier qu’il existe bien dans la table des symboles et que c’est bien un tableau (cf. class TableSymbole) . Ensuite nous comptons le nombre d’argument et vérifions que ces derniers sont tous de type entier. Nous regardons aussi s’il n’y a pas d’erreur de syntaxe ( une virgule est absente ... ). Lorsque tout est OK, nous pouvons passer à la suite.

Voici un schéma représentant la grammaire que nous utilisons pour reconnaître une expression :

La création de l’arbre

Nous l’avons implanté grâce à des fonctions récursives qui vont générer l’arbre au fur et à mesure. Cela ressemble aux automates : suivant le caractère lu nous passons dans un autre état en appelant une autre fonction récursive et en construisant l’arbre si nécessaire. Cette façon de faire nous permet de vérifier la syntaxe tout en générant l’arbre.

De plus dans la construction il nous arrive de sauvegarder des sous arbres qui ont été construits précédemment pour pouvoir ensuite les concaténer au dernier nœud créé.

Voici un exemple pour l’addition 1+2+3

Lecture de 1 => tmp = NoeudInt(1)

Lecture de + => tmp2 = Nœud(+)

Tmp1 devient le premier fils de tmp2

Lecture de 2 => tmp3 = NoeudInt(2)

Tmp3 devient le deuxième fils de tmp2

Lecture de + => tmp4 = Noeud(+)

Tmp2 devient le premier fils de tmp4

Lecture de 3 => tmp5 = NoeudInt(3)

Tmp5 devient le deuxième fils de tmp4

On retourne tmp4

Remarque : ces calculs sont en fait optimisés dans la programmation mais suivent les mêmes règles.

Voici un exemple pour la représentation grammaticale de l’expression : i+3=TOTO*tab[1,2]

dans l’arbre, où

Affectation

Ci dessous se trouve un schéma grammatical représentant une affectation.

 

Nous pouvons constater qu’une affectation est en fait la lecture d’un identificateur, puis celle d’un  := et enfin celle d’une expression.

Une vérification est faite au niveau de variable pour vérifier qu’elle existe.

IF

Le IF est une structure de test simple. Elle effectue une vérification sur un test, et exécute un des deux blocs joint.

La programmation de cette instruction est faite par la création d’un nœud de type Expression, un ou deux de type Bloc, que nous lions ensemble.

WHILE & REPEAT

Le WHILE et le REPEAT utilisent le même principe pour la programmation. Ces deux schémas nous montrent les grandes lignes.

 

 

FOR

Le FOR est basé sur le même principe avec un niveau de difficulté légèrement supérieure. Nous considèrons qu’il existe deux types de FOR, les positifs et les négatifs. Les premiers partent d’une valeur, et vont à une autre valeur en additionnant 1, les second utilisent une décrémentation du compteur. Nous pouvons donc décomposer le FOR en quatres étapes : Une affectation, un test, un bloc et une incrémentation ou décrémentation. On a donc le schéma suivant :

APPELS de fonctions & procédures

Les appels n’utilisent pas de grands concept lors de la création de l’arbre. Il suppose tout de même un certain nombre de vérification, avant la création. En effet il faut vérifier l’existence de la fonction appelée. Puis vérifier que cette fonction comporte bien les bons paramètres. Il faut ensuite créer un arbre de la forme du schéma.

Entrées/sorties avec READ et WRITE

Ces deux fonctions ne créent pas d’arbre complexe, mais juste un nœud. Il est composé d’une référence dans la table des symboles, soit pour pointer l’objet qui recevra le résultat du READ, soit pour indiquer l’objet à afficher. Ces fonctions n’acceptent qu’un seul argument, contrairement au Turbo Pascal.

 

3.6.Conversion de type

L’analyse sémantique a pour but de vérifier le sens d’un programme syntaxiquement correct. Elle doit donc inspecter si les variables sont bien déclarées et non dupliquées. Or, nous avons choisi pour des raisons d’optimisation de vérifier ces deux conditions à l’étape de l’analyse syntaxique. L’analyse sémantique de notre compilateur Pascal se résume donc à la conversion des types.

La conversion de type consiste à insérer des nœuds de transformation et à expliciter le type de certains opérateurs. Les nœuds de conversion peuvent contenir au sein du champ type l’information suivante :

Ä REAL_TO_INTEGER,

Ä INTEGER_TO_REAL,

Ä INTERGER_TO_BOOLEAN,

Ä BOOLEAN_TO_INTEGER,

Ä REAL_TO_BOOLEAN,

Ä BOOLEAN_TO_REAL,

Pour des raisons de commodité, nous avons géré le type BOOLEAN de la même façon qu’un type INTEGER.

La première étape consiste à rechercher pour chaque instruction stockée dans l’arbre les types prioritaires.

 

3.6.1.Recherche du type prioritaire

Deux cas doivent être distingués :

Ä Pour une affectation, le type des instructions situées à droite de l’affectation doit être identique au type de la variable ou du tableau situé à gauche de l’affectation. Le type prioritaire est donc celui de la variable ou du tableau.

Ä Pour les instructions de comparaisons (>, <, >=, <=, <>, =, …, OR, XOR, AND, NOT), le type prioritaire doit être recherché avant de réaliser les insertions de conversion.

 

 

 

 

 

 

 

 

 

 

Arbre représentant une instruction <

 

Dans l’exemple ci-dessus, nous voyons qu’il est nécessaire de parcourir le sous-arbre de l’instruction pour déterminer que REAL est le type prioritaire.

La deuxième étape consiste à reparcourir l’arbre pour insérer les nœuds de conversion et pour modifier le type des opérateurs.

 

3.6.2.Insertion des nœuds de conversion et modification du type des opérateurs

Lorsque le type prioritaire d’une instruction a été déterminé, chaque nœud dont le type est différent du type prioritaire doit être converti.

Dans l’exemple précédent, l’insertion d’un nœud " INTERGER_TO_REAL " doit être effectué et le type de l’opérateur doit être modifié.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Avant la conversion Après la conversion

 

Lorsque la conversion à réaliser est de type REAL_TO_INTEGER, INTERGER_TO_BOOLEAN, BOOLEAN_TO_INTEGER, REAL_TO_BOOLEAN, il n’y a pas de modification du type de l’opérateur à effectuer pour deux raisons. La première est que nous avons géré les types BOOLEAN de la même manière que les types INTEGER. La deuxième est que si un type opérateur n’est pas réel, nous le considérons au niveau de la génération de code comme un opérateur entre deux entiers.

3.7.Les listes chainées

Cette liste est composée d’une classe List trouvée sur internet, dans laquelle nous avons rajouté des fonctions utiles pour notre projet de compilation.

Nous avons donc implémenté deux classes :

ListElement : Cette classe nous permet en fait de créer des maillons de type TableSymbole, qui serviront de support pour la liste.

Les données membres sont

Le maillon suivant : de type ListElement

L'objet du maillon : de type TableSymbole

List : Cette classe génère une liste, et fournit un grand nombre d'outil pour l'utiliser

Les données membres sont

le premier maillon de la liste : de type ListElement

le dernier maillon de la liste : de type ListElement

 

Dans le schéma ci-dessous se trouve la représentation d’une liste avec nos classes

Voici une rapide énumération des fonctions disponibles dans la classe List :

IsEmpty

InserDebut

InsertFin

EnleveDebut

EnleveFin

AjouteListDebut

AjouterFin

RechercheCharList

RechercheRefList

Ref2CharList

Char2RefList

 

Comment fonctionne cette liste ?

A la création, cette liste est vide. Quand nous lui rajoutons un élément (un maillon), nous pouvons soit le mettre en début, soit en fin de liste. Une remarque très important est que les éléments ne sont pas triés. Nous avons aussi la possibilité de concaténer deux listes différentes. Pour la suppression d’un maillon, nous enlevons soit le premier, soit le dernier. Mais nous n’avons pas implanté de fonctions de suppressions d’un maillon n’importe où dans la liste. Pourquoi ?. Parce que nous ne traitons que des blocs de maillon, c’est à dire des suites successives de maillons. Et lorsqu’il faut les enlever de la liste, il suffit de les retirer par là où ils ont été inséré.

Il nous est aussi possible de savoir à tout moment si une liste est vide grâce à une fonction appropriée.

Il y a de plus des fonctions qui font appels à la classe TableSymbole : Ces fonctions permettent de rechercher dans la liste le maillon correspondant à une certaine demande, et renvoie son adresse.

4.Génération du code intermédiare

 

Etape 4 : génération du code intermédiaire

 

4.1.Choix du code généré

Dans un premier temps, nous avions pensé générer du code pour la machine virtuelle Java. Rapidement, nous nous sommes rendus compte que cela allait engendrer de nombreuses difficultés liées aux contrôles de sécurité qu’effectue le simulateur Java avant et pendant l’exécution du code.

Pour ces raisons, nous avons préféré diriger notre choix sur la génération d’un code assembleur pour processeurs Mips.

Les avantages de notre choix :

·  Très bonne lisibilité du code produit

·  Pas besoin de définir sa propre machine, ce qui a représenté pour nous un gain de temps

·  Certitude d’utiliser un langage possédant toutes les instructions nécessaires

·  Possibilité d’utiliser Xspim, un simulateur avec un débugger intégré afin de trouver plus rapidement nos erreurs

·  Meilleur approche des véritables compilateurs en générant un assembleur existant

Les inconvénients de notre choix :

·  Contraintes : se plier à l’existant

·  Difficultés liées à l’utilisation d’une machine à registres comme une machine à pile

Le code généré fonctionne donc principalement à l’aide de la pile. Cependant l’utilisation de six registres reste indispensable. Quatre afin de pouvoir exécuter les instructions Mips qui nécessitent comme opérandes des registres, $s0 et $s1 afin d’effectuer les calculs sur les entiers, puis $f0 et $f2, registres du copossesseur mathématique pour réaliser les calculs sur les réels. Les deux derniers registres utilisés sont $sp, le pointeur de pile et $s7, le pointeur sur le dernier bloc de variables locales alloués.

 

4.2.Principes de génération du code

Comme le montre le schéma précédant, la génération du code est réalisée à l’aide de l’arbre et des tables de symboles que lui fournit l’analyse sémantique. Il est donc nécessaire d’interpréter cet arbre en lui associant les instructions assembleurs correspondantes.

Intéressons nous au code généré par les instructions suivantes :

if ( a = 12 + b ) then

begin

...

end

else

begin

...

end;

Voici l’arbre (simplifié) généré par ces instructions :

Représentation logique Représentation physique

Voici le code (simplifié) généré par cet arbre :

â  Afficher le commentaire " Traitement d’un If " Généré par le noeud If en 1

â  Empiler la valeur de la variable a Généré par le noeud a en 2

â  Empiler l’entier 12 Généré par le noeud 12 en 3

â  Empiler la valeur de la variable b Généré par le noeud b en 4

â  Additionner les deux derniers éléments sur Généré par le noeud + en 5

la pile, les dépiler et empiler le résultat

de l’addition

â  Tester l’égalité des deux derniers éléments Généré par le noeud = en 6

sur la pile, les dépiler et empiler le résultat

du test.

 

â  Tester la dernière valeur sur la pile, Généré par le noeud If en 7

la dépiler. Si cette valeur est " vrai ", aller

à l’étiquette If, sinon à l’étiquette Else

Etiquette If :

â  Insersion du bloc If Généré par les noeuds du bloc en 8

â  Aller à l’étiquette Fin Généré par le noeud If en 9

Etiquette Else :

â  Insertion du bloc Else Généré par les noeuds du bloc en 10

Etiquette Fin : Généré par le noeud If en 11

 

Principe théorique de génération du code à partir de l’arbre logique :

· Pour chaque feuille rencontrée, générer son code assembleur associé

· Pour chaque nœud rencontré, compter le nombre de fois ou le nœud est touché par la récursivité et produire le code associé au nœud en fonction de ce nombre.

Dans l’exemple précédant, le nœud If est touché quatre fois par la récursivité. Une première fois en arrivant de son père et trois fois en remontant de ses fils.

Principe utilisé pour la génération du code à partir de l’arbre physique :

Cette nouvelle représentation de l’arbre nous pose ici un problème évidant. Afin d’implémenter le nœud If, il faut générer quatre portions de code entre lesquels vont s’insérer le code généré par les fils. Malheureusement, le nœud n’est rencontré que deux fois, une fois à la descente et une fois à la remonté de la récursivité.

Solution :

· Chaque nœud génère son propre code. Cas des nœuds 1, 2, 3, 4, 5, 6, 8 et 10 de l’exemple précédant.

· Les nœuds, lorsqu’ils sont parcourus proposent à leur père de générer du code s’il le souhaite. Cas des nœuds 7 ,9 et 11 de l’exemple précédant.

 

4.3.Les variables globales et locales

Les variables globales sont déclarées dès la compilation et sont stockées dans le segment de donné. Les variables locales sont allouées et désallouées sur la pile au cours de l’exécution du code assembleur.

 

4.4.Procédures, Fonctions et récursivité

A chaque appel d’une procédure/fonction et à plus forte raison, lors d’un appel récursif, il faut allouer un nouvel espace pour les variables locales. Les variables passées en paramètre à ces procédures/fonctions sont gérées de la même manière que les variables locales à la seule différence qu’elles sont initialisées par les arguments de la procédure/fonction.

L’allocation d’espace pour les variables locales est effectuée durant l’exécution du programme. Lors de l’appel d’une procédure/fonction, un bloc d’espace est réservé sur la pile. A la compilation, nous ne connaissons pas l’adresse des variables locales mais nous connaissons pour chaque variable son emplacement relatif par rapport au début du bloc courant. Ainsi nous avons lors de l’exécution du programme toujours accès aux variables en additionnant le déplacement relatif de la variable recherchée à la valeur du registre $s7 qui représente un pointeur sur le début du bloc courant. Ce pointeur est soigneusement remis à jour sur les bons blocs au fur et à mesure des appels.

Exemple de Pile d’exécution

Lors de l’appel successif de la même procédure/fonction, il est évidant grâce au principe cité ci-dessus, que l’utilisation des variables dans les procédures/fonctions englobées n’a aucune influence sur les valeurs des variables des procédures/fonctions englobantes. Les variables globales sont quant à elles uniques et leurs modifications est visible dans tout le programme.

 

4.5.Génération des labels

Tout au long de la génération du code assembleur, il est nécessaire d’utiliser des labels. Ces labels sont donc générés en même temps que le code. Les labels fonctionnent par groupe, un label unique est donc utilisé un nombre de fois déterminé, à des emplacements déterminés. Un nombre inconnu de label peuvent être généré entre deux labels devant être identiques (cas de If imbriqué, etc ...). Il est donc impossible d’utiliser une fonction GénérerUnLabel() qui retournerait le label approprié au bon moment. Nous avons donc choisi d’utiliser une fonction GénérerLabels() qui empile tous les labels nécessaires à un moment donné, et une fonction PrendreLabel() qui renvoie un label après l’avoir dépilé sur cette pile.

 

4.6.Codage d’un flottant sur Mips

Le Mips étant un processeur Risc, il possède un jeu d'instructions limité. Cela nous pose un problème lorsque nous souhaitons placer dans un registre un flottant car il n'existe pas d'instruction prévue à cet effet. L'astuce est de coder dans un premier temps ce flottant sur un entier de 32 bits en utilisant la notation suivante.

La représentation d'un réel sous Mips est définie par la norme IEEE. Sa forme générale est la suivante:

(-1)S x (1+signifiant) x 2 E

Ä S représente le signe,

Ä signifiant est une fraction entre 0 et 1,

Ä E est l'exposant.

En appliquant cette formule, un flottant simple précision est stockée sous Mips au sein de 32 bits :

signe exposant signifiant

Prenons comme exemple le réel -0.75.

Sous forme de fraction ce chiffre est représenté par

-0.75 = -3 x 2 -2 = -11 x 2 -2 = -1.1 x 2 -1.

Sa représentation générale est donc

(-1)1 x (1 + .1000 0000 0000 0000 0000 000) x 2 (-1+127).

En binaire :

signe exposant signifiant

En hexadécimal

signe exposant signifiant

La nouvelle représentation hexadécimale de nos réels nous permettra grâce à l'instruction li (load immédiat) de les charger dans un registre.

 

5.Un exemple de A à P...

Nous allons montrer les différentes étapes de la compilation d’un fichier Pascal, calculant le PGCD de deux nombres.

5.1.Le fichier Pascal

program pgcd ;

var nombre_1 : integer;

var nombre_2 : integer;

begin

write ('Programme calculant le PGCD\n' );

write ('---------------------------\n\n');

write ('Valeur du premier nombre : ' );

read(nombre_1);

write ('\nValeur du deusieme nombre : ' );

read( nombre_2 );

while (nombre_1 <> nombre_2) do

begin

if (nombre_1 > nombre_2) then

begin

nombre_1 := nombre_1 - nombre_2;

end

else

begin

nombre_2 := nombre_2 - nombre_1;

end;

end;

write ('\nLe PGCD des deux nombres est : ' );

write (nombre_1);

end.

 

5.2.Passe 1 : Lex

$037 $090 pgcd$ $084

$052 $090 nombre_1$ $059 $022 $084

$052 $090 nombre_2$ $059 $022 $084

$005

$055 $071 $089 'Programme calculant le PGCD\n'$ $072 $084

$055 $071 $089 '---------------------------\n\n'$ $072 $084

$055 $071 $089 'Valeur du premier nombre : '$ $072 $084

$039 $071 $090 nombre_1$ $072 $084

$055 $071 $089 '\nValeur du deusieme nombre : '$ $072 $084

$039 $071 $090 nombre_2$ $072 $084

$053 $071 $090 nombre_1$ $062 $090 nombre_2$ $072 $014

$005

$021 $071 $090 nombre_1$ $068 $090 nombre_2$ $072 $045

$005

$090 nombre_1$ $058 $090 nombre_1$ $076 $090 nombre_2$ $084

$017

$016

$005

$090 nombre_2$ $058 $090 nombre_2$ $076 $090 nombre_1$ $084

$017 $084

$017 $084

$055 $071 $089 '\nLe PGCD des deux nombres est : '$ $072 $084

$055 $071 $090 nombre_1$ $072 $084

$017 $073

 

 

5.3.Passe 2 : Syntaxique & Sémantique

 

5.4.Passe 3 : Le code

.data

nombre_1: .space 4

nombre_2: .space 4

Progr: .asciiz "Programme calculant le PGCD\n"

_____: .asciiz "---------------------------\n\n"

Valeu: .asciiz "Valeur du premier nombre : "

_nVal: .asciiz "\nValeur du deusieme nombre : "

_nLe_: .asciiz "\nLe PGCD des deux nombres est : "

.text

.globl main

main: addi $sp,-4 # Ajoute un element sur la pile

sw $ra,($sp) # Empile l'adresse de retour

# Ecriture d'une chaine de caracteres

li $v0,4 # Service systeme : Afficher une chaine

la $a0,Progr # Adresse de la chaine

syscall # Affichage de la chaine

# Ecriture d'une chaine de caracteres

li $v0,4 # Service systeme : Afficher une chaine

la $a0,_____ # Adresse de la chaine

syscall # Affichage de la chaine

# Ecriture d'une chaine de caracteres

li $v0,4 # Service systeme : Afficher une chaine

la $a0,Valeu # Adresse de la chaine

syscall # Affichage de la chaine

# Lecture d'un entier

li $v0,5 # Service systeme : Lecture d'un entier

syscall # Lecture de l'entier

la $s0,nombre_1 # Lecture de l'adresse de la variable

sw $v0,($s0) # Ecriture dans la variable

# Ecriture d'une chaine de caracteres

li $v0,4 # Service systeme : Afficher une chaine

la $a0,_nVal # Adresse de la chaine

syscall # Affichage de la chaine

# Lecture d'un entier

li $v0,5 # Service systeme : Lecture d'un entier

syscall # Lecture de l'entier

la $s0,nombre_2 # Lecture de l'adresse de la variable

sw $v0,($s0) # Ecriture dans la variable

# While(CONDITION) do BLOCK

l0001: # --> Insertion de l'evaluation de la condition

# Empile la valeur d'une variable

addi $sp,-4 # Ajoute un element sur la pile

la $s0,nombre_1 # Lecture de l'adresse de la variable

lw $s0,($s0) # Lecture de la valeur de la variable

sw $s0,($sp) # Empile la variable

# Empile la valeur d'une variable

addi $sp,-4 # Ajoute un element sur la pile

la $s0,nombre_2 # Lecture de l'adresse de la variable

lw $s0,($s0) # Lecture de la valeur de la variable

sw $s0,($sp) # Empile la variable

# Test si 4($sp) <> ($sp)

lw $s0,4($sp) # Lecture de l'expression gauche

lw $s1,($sp) # Lecture de l'expression droite

sne $s0,$s0,$s1 # Test si <>

sw $s0,4($sp) # Empile le resultat

addi $sp,4 # Retire un element sur la pile

li $s0,0 # Pour tester si CONDITION fausse

lw $s1,($sp) # Resultat de l'expression

addi $sp,4 # Retire un element sur la pile

beq $s0,$s1,l0000 # Branche si CONDITION=FAUX

# --> Insertion du block

# If(CONDITION) then BLOCK else BLOCK

# --> Insertion de l'evaluation de la condition

# Empile la valeur d'une variable

addi $sp,-4 # Ajoute un element sur la pile

la $s0,nombre_1 # Lecture de l'adresse de la variable

lw $s0,($s0) # Lecture de la valeur de la variable

sw $s0,($sp) # Empile la variable

# Empile la valeur d'une variable

addi $sp,-4 # Ajoute un element sur la pile

la $s0,nombre_2 # Lecture de l'adresse de la variable

lw $s0,($s0) # Lecture de la valeur de la variable

sw $s0,($sp) # Empile la variable

# Test si 4($sp) > ($sp)

lw $s0,4($sp) # Lecture de l'expression gauche

lw $s1,($sp) # Lecture de l'expression droite

sgt $s0,$s0,$s1 # Test si >

sw $s0,4($sp) # Empile le resultat

addi $sp,4 # Retire un element sur la pile

li $s0,0 # Pour tester si CONDITION fausse

lw $s1,($sp) # Resultat de l'expression

addi $sp,4 # Retire un element sur la pile

beq $s0,$s1,l0003 # Branche si CONDITION=FAUX

# --> Insertion du block if

# Empile l'adresse d'une variable

addi $sp,-4 # Ajoute un element sur la pile

la $s0,nombre_1 # Lecture de l'adresse de la variable

sw $s0,($sp) # Empile la variable

# Empile la valeur d'une variable

addi $sp,-4 # Ajoute un element sur la pile

la $s0,nombre_1 # Lecture de l'adresse de la variable

lw $s0,($s0) # Lecture de la valeur de la variable

sw $s0,($sp) # Empile la variable

# Empile la valeur d'une variable

addi $sp,-4 # Ajoute un element sur la pile

la $s0,nombre_2 # Lecture de l'adresse de la variable

lw $s0,($s0) # Lecture de la valeur de la variable

sw $s0,($sp) # Empile la variable

# Soustraction de deux entiers

lw $s0,4($sp) # Lecture de la premiere operande

lw $s1,($sp) # Lecture de la seconde operande

neg $s1,$s1 # Ajout du signe moins devant l'entier

add $s0,$s0,$s1 # Addition des deux entiers

sw $s0,4($sp) # Empile le resultat

addi $sp,4 # Retire un element sur la pile

# Affectation 4($sp) := ($sp)

lw $s0,4($sp) # Lecture de l'expression gauche

lw $s1,($sp) # Lecture de l'expression droite

sw $s1,($s0) # Ecrit le resultat

addi $sp,8 # Retire deux elements sur la pile

j l0002

l0003: # --> Insertion du block else

# Empile l'adresse d'une variable

addi $sp,-4 # Ajoute un element sur la pile

la $s0,nombre_2 # Lecture de l'adresse de la variable

sw $s0,($sp) # Empile la variable

# Empile la valeur d'une variable

addi $sp,-4 # Ajoute un element sur la pile

la $s0,nombre_2 # Lecture de l'adresse de la variable

lw $s0,($s0) # Lecture de la valeur de la variable

sw $s0,($sp) # Empile la variable

# Empile la valeur d'une variable

addi $sp,-4 # Ajoute un element sur la pile

la $s0,nombre_1 # Lecture de l'adresse de la variable

lw $s0,($s0) # Lecture de la valeur de la variable

sw $s0,($sp) # Empile la variable

# Soustraction de deux entiers

lw $s0,4($sp) # Lecture de la premiere operande

lw $s1,($sp) # Lecture de la seconde operande

neg $s1,$s1 # Ajout du signe moins devant l'entier

add $s0,$s0,$s1 # Addition des deux entiers

sw $s0,4($sp) # Empile le resultat

addi $sp,4 # Retire un element sur la pile

# Affectation 4($sp) := ($sp)

lw $s0,4($sp) # Lecture de l'expression gauche

lw $s1,($sp) # Lecture de l'expression droite

sw $s1,($s0) # Ecrit le resultat

addi $sp,8 # Retire deux elements sur la pile

l0002:

j l0001

l0000:

# Ecriture d'une chaine de caracteres

li $v0,4 # Service systeme : Afficher une chaine

la $a0,_nLe_ # Adresse de la chaine

syscall # Affichage de la chaine

# Ecriture d'un entier

li $v0,1 # Service systeme : Afficher un entier

la $s0,nombre_1 # Lecture de l'adresse de la variable

lw $a0,($s0) # Lecture de la valeur de la variable

syscall # Affichage de l'entier

lw $ra,($sp) # Depile l'adresse de retour

addi $sp,4 # Retire un element sur la pile

jr $ra # Retour au systeme appellant

 

6.Documentation de l’utilisateur de P2Mips

6.1.La structure de base

program prog;

...

begin

...

end.

 

6.2.Les déclarations

var a : integer;

var b : boolean;

var c : real;

const CONSTANTE = 4;

type tab = array[1..4,3..5] of integer;

 

6.3.Les boucles

for a:=1 to 10 do

begin

...

end;

for a:=10 downto 1 do

begin

...

end;

while a=1 do

begin

...

end;

repeat

...

until a=1;

if (a=1) then

begin

...

end;

if (a<1) then

begin

...

end

else

begin

...

end;

if not (a and b or c xor d) then

begin

...

end;

 

6.4.Les opérations

a := a + 1;

c := 4.7 - 1.8;

c := a * 1.8;

a := 2 / 1.8;

a := 1 + tab[1,4];

b:= true and not false;

 

6.5.Les procédures fonctions

procedure proc (arg1 : boolean);

procedure proc (arg1 : boolean)

begin

...

end;

function func ( arg1 :real; arg2 : integer) : boolean;

function func ( arg1 :real; arg2 : integer) : boolean

begin

...

end;

 

6.6.Les entrées/sorties

read(a);

write(a);

write(‘Ceci est une chaine de caractere\n’);

 

7.Bilan

Nous sommes heureux d’avoir achevé ce projet de compilation dans les temps impartis. Nous en avons retiré un enrichissement personnel. Il nous à permis d’approfondir et de consolider nos connaissances en compilation. Les résultats obtenus sont très satisfaisants puisque d’un programme source Pascal nous sommes arrivés à générer un fichier en assembleur qui est exécuté par le simulateur Xspim sur Es2i. Ces résultats positifs n’ont pas été obtenu sans peine, nous estimons y avoir passé 350 heures homme soit environ 75 heures par personne.

L’étude de la conception à été la phase la plus difficile mais le temps consacré sur cette partie était indispensable, nous n’y avons pas rencontré d’erreur fondamentale par la suite.

L’analyse lexicale grâce à Lex a abouti rapidement.

De part notre travail de programmation orienté objet, nous avons rencontré quelques difficultés dans la réalisation des fonctions permettant l’analyse sémantique car elles vérifient la syntaxe et construisent simultanément l’arbre associé.

Pour la sémantique, nous avons effectué des conversions de type, les autres étapes qui incombent généralement à cette partie ayant été réalisées pendant l’analyse syntaxique et la génération du code intermédiaire.

La complexité de la génération du code intermédiaire a résidé surtout dans la réalisation simultané du code assembleur et du code permettant de le générer ainsi que la gestion des variables locales.

Une bonne organisation du groupe et son dynamisme nous a permis d’effectuer un assemblage rapide des différentes parties réalisées par chacun d’entre nous. Une distribution homogène des taches au sein du groupe nous a permis d’interagir sur les programmes pour assurer leur compatibilité.

Ayant abordé les principaux points stratégiques de la conception, nous avons décidé de l’interrompre, malgré l’absence de certaines fonctionnalités mineures.