Compilateur - Génération de code intermédiaire

Un code source peut être directement traduit dans son code machine cible, alors pourquoi avons-nous besoin de traduire le code source en un code intermédiaire qui est ensuite traduit en son code cible? Voyons les raisons pour lesquelles nous avons besoin d'un code intermédiaire.

  • Si un compilateur traduit le langage source vers son langage machine cible sans avoir la possibilité de générer du code intermédiaire, alors pour chaque nouvelle machine, un compilateur natif complet est requis.

  • Le code intermédiaire élimine le besoin d'un nouveau compilateur complet pour chaque machine unique en gardant la partie d'analyse identique pour tous les compilateurs.

  • La deuxième partie du compilateur, la synthèse, est modifiée en fonction de la machine cible.

  • Il devient plus facile d'appliquer les modifications du code source pour améliorer les performances du code en appliquant des techniques d'optimisation du code sur le code intermédiaire.

Représentation intermédiaire

Les codes intermédiaires peuvent être représentés de diverses manières et ils ont leurs propres avantages.

  • High Level IR- La représentation du code intermédiaire de haut niveau est très proche du langage source lui-même. Ils peuvent être facilement générés à partir du code source et nous pouvons facilement appliquer des modifications de code pour améliorer les performances. Mais pour l'optimisation de la machine cible, c'est moins préféré.

  • Low Level IR - Celui-ci est proche de la machine cible, ce qui le rend approprié pour l'allocation de registre et de mémoire, la sélection de jeu d'instructions, etc. Il est bon pour les optimisations dépendant de la machine.

Le code intermédiaire peut être soit spécifique au langage (par exemple, Byte Code pour Java), soit indépendant du langage (code à trois adresses).

Code à trois adresses

Le générateur de code intermédiaire reçoit l'entrée de sa phase prédécesseur, l'analyseur sémantique, sous la forme d'un arbre de syntaxe annoté. Cet arbre de syntaxe peut alors être converti en une représentation linéaire, par exemple, la notation postfixe. Le code intermédiaire a tendance à être un code indépendant de la machine. Par conséquent, le générateur de code suppose d'avoir un nombre illimité de stockage mémoire (registre) pour générer du code.

Par exemple:

a = b + c * d;

Le générateur de code intermédiaire essaiera de diviser cette expression en sous-expressions puis de générer le code correspondant.

r1 = c * d;
r2 = b + r1;
a = r2

r utilisé comme registres dans le programme cible.

Un code à trois adresses a au plus trois emplacements d'adresses pour calculer l'expression. Un code à trois adresses peut être représenté sous deux formes: quadruples et triples.

Quadruples

Chaque instruction de présentation en quadruples est divisée en quatre champs: opérateur, arg1, arg2 et résultat. L'exemple ci-dessus est représenté ci-dessous au format quadruples:

Op arg 1 arg 2 résultat
* c r1
+ b r1 r2
+ r2 r1 r3
= r3 une

Triples

Chaque instruction de la présentation en triplets comporte trois champs: op, arg1 et arg2. Les résultats des sous-expressions respectives sont indiqués par la position de l'expression. Les triplets représentent la similitude avec le DAG et l'arbre syntaxique. Ils sont équivalents à DAG lorsqu'ils représentent des expressions.

Op arg 1 arg 2
* c
+ b (0)
+ (1) (0)
= (2)

Les triples sont confrontés au problème de l'immobilité du code lors de l'optimisation, car les résultats sont positionnels et la modification de l'ordre ou de la position d'une expression peut poser des problèmes.

Triples indirects

Cette représentation est une amélioration par rapport à la représentation en triplets. Il utilise des pointeurs au lieu de la position pour stocker les résultats. Cela permet aux optimiseurs de repositionner librement la sous-expression pour produire un code optimisé.

Déclarations

Une variable ou une procédure doit être déclarée avant de pouvoir être utilisée. La déclaration implique l'allocation d'espace en mémoire et l'entrée du type et du nom dans la table des symboles. Un programme peut être codé et conçu en gardant à l'esprit la structure de la machine cible, mais il n'est pas toujours possible de convertir avec précision un code source dans sa langue cible.

En prenant l'ensemble du programme comme un ensemble de procédures et de sous-procédures, il devient possible de déclarer tous les noms locaux à la procédure. L'allocation de mémoire se fait de manière consécutive et les noms sont alloués à la mémoire dans l'ordre où ils sont déclarés dans le programme. Nous utilisons la variable offset et la mettons à zéro {offset = 0} qui indique l'adresse de base.

Le langage de programmation source et l'architecture de la machine cible peuvent varier dans la façon dont les noms sont stockés, donc un adressage relatif est utilisé. Alors que la mémoire est allouée au premier nom à partir de l'emplacement de mémoire 0 {offset = 0}, le nom suivant déclaré plus tard doit être alloué en mémoire à côté du premier.

Example:

Nous prenons l'exemple du langage de programmation C où une variable entière se voit attribuer 2 octets de mémoire et une variable flottante se voit attribuer 4 octets de mémoire.

int a;
float b;

Allocation process:
{offset = 0}

   int a;
   id.type = int
   id.width = 2

offset = offset + id.width 
{offset = 2}

   float b;
   id.type = float
   id.width = 4
   
offset = offset + id.width 
{offset = 6}

Pour saisir ce détail dans une table de symboles, une entrée de procédure peut être utilisée. Cette méthode peut avoir la structure suivante:

enter(name, type, offset)

Cette procédure doit créer une entrée dans la table des symboles, pour le nom de la variable , dont le type est défini sur type et le décalage d' adresse relatif dans sa zone de données.