Traduction

La création d’un programme C à partir de plusieurs unités de compilation passe par huit phases sucessives de traduction.

Avant la compilation proprement dite les sauts de lignes sont agglomérés quand besoin et convertis en sauts de ligne génériques qui serviront au prépocesseur.

Après le préprocesseur les sauts de lignes restants et l’espace blanc sont supprimés et les sauts de ligne encodés dans les chaines de caractères sont encodés en sauts de ligne de l’environnement d’éxécution et vers le jeu de caractère de l’envionnement d’exécution (ASCII, EBCDIC…).

Puis les chaines de caractères adjacentes sont agglomérées puis la compilation et la liaison proprement dite inteviennent.

Phases normales

Une phase s’exécute entèrement puis passe la main à la suivante.

Une phase exécutée il n’est plus possible de revenir en arrière. Si un fichier est inclus par le préprocesseur , la traduction reprend à la phase 1 pour le fichier inclus, cela pour chaque fichier inclus récursivement et successivement.

Si une erreur survient lors d’une des phases, la traduction peut s’arrêter.

Phase 1 : trigraphes et sauts de ligne

Les caractères multioctet du fichier physique sont convertis vers le jeu de caractères du fichier source . Cela concerne principalement les séquences de fin de ligne sur plusieurs octets (\r\n) qui sont convertis en fin de ligne virtuel .

Les trigraphes sont remplacés (jusqu'au c23 ).

Phase 2 : barres anti obliques

Les sauts deligne immédiatement précédes du caractère anti oblique \ sont supprimmés.

C’est un processus à une seule passe :

"toto\\
ntata"

produit "toto\ntata" et non "totontata".

Un fichier source non vide doit se terminer avec un saut de ligne, qui ne doit pas être précédé d’une contre oblique.

Un fichier dont l’unique contenu est

int main() {} /* noter la contreoblique terminale */\

est illégal. De même si ce fichier ne contenait pas un saut de ligne terminal.

La suppression des commentaires a lieu après cette phase, si bien que une barre contre-oblique terminale dans un commentaire mono ligne ne fait pas partie du commentaire

int main() {} // l'antishash suivant n'est pas commenté\
cette ligne fait toujours partie du commentaire\
ainsi que toutes les lignes suivantes tant qu'elles\
sont rpécédées d'une contre-oblique.

Phase 3 : commentaires et décomposition lexicale

Chaque commentaire est converti en un espace. Les sauts de ligne sont conservés. Il n’est pas requis par la norme que les espacesz successifs soient regroupés en un seul.

Les caractères successifs du fichier sont regroupés en lexons de préprocesseur (preprocessing tokens), ou unités syntaxiques du préprocesseur. Les syntagmes peuvent contenir des éléments du préprocesseur (#, elif, defined, < nom de fichier >, sauts de ligne…), de l’espace blanc, et des éléments du langage C (while, int, ++, 15.5e+10, "bonjour", *, ==, etc.)

Le découpage se fait de manière gourmande en considérant le plus long élément de grammaire qu’il soit possible de constituer. C’est à dire que === sera découpé en == et =, et non en =, = et =, ou = et ==, ==== sera découpé en == et == (deux opérateurs d´égalité successifs), +====+++-= sera découpé en +=, ==, =, ++, + et -=.

La validité de la syntaxe, que ce soit celle du langage préprocesseur ou du langage C n’est pas prise en compte lors du découpage.

i+++++i sera découpé ainsi i++ ++ +i et non i++ + ++i, donc ce code ne passera pas la phase 7 (compilation). Pour aider au découpage on peut ajouter de l’espace entre les éléments de grammaire : i+++ ++i.

Un fichier ne doit pas se terminer en un commentaire ou un syntagme incomplet : "toto, /* commentaire ou 12.12e à la fin d’un fichier devrait stopper la traduction.

L’absence du’un point-virgule ou toute autre invalidité grammaticale n’est pas considéré comme un syntagme incomplet et une cause d’arrêt à ce point. On peut donc inclure des fichiers qui ne contiennent pas de code C grammaticalement correct à condition qu’après inclusion le code le devienne

// fichier inc.h
"toto"  // <- noter l'absence de point-virgule
//fichier main.c
#include <stdio.h>

int main() {
    puts(
#include "inc.h"
    );
}

Le résultat est

int main() {
    puts(
"toto"
    );
;
}

et le programme est correct.

Par contre

// fichier inc.h, noter le syntagme incomplet
"to  
//fichier main.c
#include <stdio.h>

int main() {
    puts(
#include "inc.h"
to");
}

cause l’arrêt de la traduction du fichier inclus à la phase 3, et donc ignore la phase 4, et on n’obtient pas

int main() {
    puts(
"to
to"
    );
;
}

Ce qui est d’ailleurs illégal car on ne peut avoir de saut de ligne non échapé (\n) dans une chaine de caractères.

Si on souhaite concaténer une chaine répatie sur plusieurs fichiers, ceci est possible

// fichier inc.h
"to"
//fichier main.c
#include <stdio.h>

int main() {
    puts(
#include "inc.h"
    "to");
}

On obtient

int main() {
    puts(
"to"
    "to");
}

qui après la phase 6 devient

int main() {
    puts(
"toto");
}

Il est laissé à la discrétion de l’implémtantation de regrouper tous les caractères d’espacement en une seule espace ou de les laisser tels quels : cela n’a aucune incidence sur la validité grammaticale du code ni sur le processus de traduction.

Phase 4 : préprocesseur

Le préprocesseur traite le fichier source. Si un fichier est inclus par le préprocesseur, celui ci passe par les étapes de traduction 1 à 4, cela récursivement.

Un fichier unique regroupant tous les fichiers inclus et dont toutes les directives de préprocesseur ont été exécutées et enlvées du code source est passé à la phase suivante de la traduction.

A la fin de cette phase toutes les directives du préprocesseur sont supprimées ainsi que toutes les variables définies par celui-ci, et leurs valeurs associées.

🧨 Il est courant de croire que les variables définies dans un fichier débordent sur tout le projet. En réalité chaque unité de compilation ne travaille qu’avec ses propres variables de préprocesseur, qui sont crées au début de la phase 4 et celles-ci disparaissent à la fin de la phase 4, ceci autant de fois qu’il y a d’unités de compilation. Les fichiers inclus récursivement et qui repassent par les phases 1 à 4 héritent toutefois des variables définies dans l’itération précédente de la récursion.

Phase 5: conversion du jeu source en jeu d’exécution

Les caractères échappés et sauts de ligne dans les chaines de caractère et les caractères isolés sont convertis en leur valeur réelle et en sauts de ligne de l’environnement d’exécution.

Phase 6: concaténation de chaines

Les chaines de caractère adjacentes sont groupées en use seule.

C’est a dire que "bonjour" " monde" produit "bonjour monde" (noter que le caractère nul à la fin de “bonjour” est élidé).

Cette phase est une nouveauté de l’ANSI C qui permet d’écrire une chaine de caractères sur plusieurs lignes, notamment sur les supports où la limite physique d’un catalogue de code source est limité mais également de produire des chaines de caractères via le langage préprocesseur.

main() {
    char * a = "une longue chaine"
    "de caractères";
    /* a = "une longue chaine de caractères" */

    char * b "une longue chaine\
de caractères";  /* La manière avant l'ANSI C 
d'écrire une chaine sur plusieurs lignes */

}

Phase 7 : compilation

L’espacement entre les syntagmes est supprimé, y compris les sauts de ligne.

Les syntagmes sont analysés, et si la grammaire du langage est respectée, du code exécutable se comportant tel que décit par le code source est produit.

Phase 8 : liaison

Toutes les références à des objets et à des fonctions externes sont résolus. Les librairies externes sont ajoutés au programme. Toutes les unités de traduction sont regroupés en un seul fichier exécutable.

Decomposition lexicale

Dans le code suivant

#define TOTO ouistiti

int _TOTO_= TOTO+++TOTO;
int  tata = "TOTO";

Les syntagmes sont int, , _TOTO_, =, , TOTO, ++, +, TOTO, ;, un saut de ligne, int, , tata, , =, , "TOTO" et ;

Le preprocesseur ne considère que des syntagmes qui correspondent entièrment à un motif de substitution.

Après la phase de preprocesseur, le code suivant est produit

int _TOTO_= ouistiti+++ouistiti;
int tata = "TOTO";

Noter que _TOTO_ et "TOTO" ne sont pas remplacés car ils ne correspondent pas entièrement et exactement au motif « TOTO ».