Cours 5 · Génération de code intermédiaireLeçon 2 sur 2
Traduction des constructions
5 h de lecture8 sections Version PDF
Expressions et affectations ; expressions booléennes en court-circuit ; if et boucles par patchage de listes ; appels de fonctions ; accès aux tableaux et aux structures.
Le chapitre 8 a linéarisé une expression en code à trois adresses. Mais un programme, ce sont
surtout des constructions de contrôle : affectations, conditions, boucles, appels. Les traduire
révèle une vérité inconfortable et féconde : au niveau intermédiaire, ni if ni while n'existent.
Il n'y a que des étiquettes et des sauts. Toute la richesse du contrôle se ramène à ce
squelette — celui du processeur.
C'est le chapitre le plus dense du bloc. On y consacre l'essentiel à if et while, les plus
importants, et à l'évaluation en court-circuit des booléens — au lieu de survoler toutes les
constructions.
Expressions et affectations
Le cas de base prolonge directement le chapitre 8. Une affectation x = e engendre le code qui
calcule e dans un temporaire, puis copie ce temporaire dans x :
x = a * b + 1 devient t1 = a * b t2 = t1 + 1 x = t2C'est de la traduction dirigée par la syntaxe (chapitre 7) : l'attribut synthétisé « adresse » remonte
de l'arbre de e, et la racine émet la copie finale. Rien de nouveau, sinon le point de destination.
Le contrôle par sauts
Voici le cœur du chapitre. Une machine ne connaît que deux primitives de contrôle, qu'on adopte au niveau intermédiaire :
ifFalse C goto L: sauter à l'étiquetteLsi la conditionCest fausse ;goto L: saut inconditionnel.
Toute structure de haut niveau se traduit en ces primitives. Le if (C) alors :
ifFalse (C) goto Lfin ... code de « alors » ...Lfin:Le if (C) alors sinon enjambe le bloc sinon par un goto après le bloc alors :
ifFalse (C) goto Lsinon ... code de « alors » ... goto LfinLsinon: ... code de « sinon » ...Lfin:Et le while (C) corps, qui teste en tête, revient tester après chaque tour :
Ldebut: ifFalse (C) goto Lfin ... code du corps ... goto LdebutLfin:Ces trois schémas sont à connaître par cœur : ce sont les briques dont tout programme impératif est
fait. L'exercice construit celui du while.
Comment traduit-on « while (C) { corps } » en code à trois adresses ?
Expressions booléennes et court-circuit
Comment évaluer la condition C elle-même, quand c'est une expression booléenne composée comme
a < b && c < d ? Deux stratégies, mais une seule est correcte pour un langage courant.
On pourrait calculer la valeur booléenne complète (évaluer a < b, puis c < d, puis le &&),
la ranger dans un temporaire, et la tester. Mais la sémantique de la plupart des langages impose
l'évaluation en court-circuit : dès que a < b est faux, le résultat du && est faux, et
c < d ne doit pas être évalué du tout.
Ce n'est pas une optimisation, c'est une exigence sémantique. Elle est vitale quand la seconde condition a un effet de bord, ou pourrait échouer :
p != NULL && p->x > 0Ici, p->x ne doit être lu que si p est non nul. Un calcul complet déréférencerait p même
quand il est nul — un plantage. Le court-circuit l'interdit.
On l'obtient en traduisant && directement en sauts, jamais en calculant un booléen :
a < b && c < d devient ifFalse (a < b) goto Lfaux ifFalse (c < d) goto Lfaux goto LvraiSi a < b est faux, on saute à Lfaux avant même d'atteindre le test de c < d. Le ||
symétrique saute vers Lvrai dès qu'une condition est vraie. C'est la seconde partie de l'exercice.
Le patchage de listes
Un problème pratique surgit à l'émission. Quand on écrit ifFalse (C) goto Lfin, l'étiquette Lfin
n'est pas encore connue : elle marque un point du code qu'on n'a pas encore engendré (un saut vers
l'avant). Comment remplir la cible d'un saut qu'on émet avant de savoir où il mène ?
La technique du patchage de listes (backpatching) répond à cela, et permet d'engendrer tout le code en une seule passe :
- on émet le saut avec une cible en blanc ;
- on retient l'instruction incomplète dans une liste (la liste des sauts à compléter) ;
- quand l'étiquette de destination devient connue, on parcourt la liste et on remplit toutes les cibles d'un coup.
C'est un mécanisme élégant mais techniquement dense — la raison pour laquelle ce bloc mérite qu'on
s'attarde sur if et while plutôt que de courir après toutes les constructions. En comprendre le
principe suffit en L3 : émettre, retenir, compléter.
Appels, tableaux, structures
Les constructions restantes se traduisent selon des schémas que l'on cite ici pour la complétude.
Un appel de fonction f(a, b) engendre le passage des paramètres puis l'appel :
param aparam bt = call f, 2 (2 = nombre d'arguments)Les détails — où vont les paramètres, comment la fonction retourne — relèvent de l'environnement d'exécution du chapitre 10.
L'accès à un tableau T[i] calcule une adresse : base(T) + i × taille_élément. C'est
pourquoi T[i] équivaut à *(T + i) — l'indexation est de l'arithmétique d'adresses. L'accès à un
champ de structure s.champ se traduit de même, par un décalage fixe connu de la table des
symboles : base(s) + décalage(champ).
Ces schémas partagent tous la même logique : ramener une construction de haut niveau à des opérations à trois adresses sur des adresses et des sauts.
Pourquoi l'évaluation en court-circuit de « p != NULL && p->x > 0 » est-elle une exigence sémantique, et non une simple optimisation ?
À vous
L'exercice traduit deux constructions au cœur du chapitre : une boucle while en étiquettes et
sauts, puis l'évaluation en court-circuit d'un &&. Vous constaterez que while n'existe pas au
niveau intermédiaire — seulement son squelette de sauts — et que le court-circuit se traduit
directement en branchements, garantissant qu'une seconde condition n'est jamais évaluée à tort.
C'est la couche du compilateur du TP qui transforme un programme structuré en une suite d'instructions et de sauts, prête pour la machine.
Traduisez une boucle while en code à trois adresses (étiquettes et sauts), puis l'évaluation en court-circuit d'un && dans la condition. Comprenez que if/while n'existent pas au niveau intermédiaire, et que le court-circuit est une exigence sémantique — pas une simple optimisation.
// Au niveau intermédiaire, il n'y a ni « while » ni « if » : seulement des // étiquettes (Ln:) et des sauts. Deux primitives suffisent : // ifFalse C goto L : sauter à L si la condition C est fausse // goto L : saut inconditionnel // // On traduit : while (x < n) { x = x + 1; } // // Schéma standard d'une boucle while : // Ldebut: <- étiquette de test // ifFalse (x < n) goto Lfin <- si faux, on sort // ...corps... // goto Ldebut <- on retourne tester // Lfin: let code3a = []; let nEtiq = 0; function nouvelleEtiquette() { return "L" + (++nEtiq); } function emettre(s) { code3a.push(s); } // ── À VOUS : traduireWhile(cond, corps) ───────────────────────────────────── // 'cond' est une chaîne (ex. "x < n"), 'corps' un tableau d'instructions déjà // engendrées (ex. ["x = x + 1"]). Émettre le schéma ci-dessus. function traduireWhile(cond, corps) { const Ldebut = nouvelleEtiquette(); const Lfin = nouvelleEtiquette(); // à compléter : // emettre(Ldebut + ":"); // emettre("ifFalse (" + cond + ") goto " + Lfin); // for (const i of corps) emettre(" " + i); // emettre("goto " + Ldebut); // emettre(Lfin + ":"); } traduireWhile("x < n", ["x = x + 1"]); console.log("while (x < n) x = x + 1;"); code3a.forEach((s) => console.log(" " + s)); // ── Court-circuit d'un && ─────────────────────────────────────────────────── // « a < b && c < d » ne s'évalue PAS en calculant les deux comparaisons : si // « a < b » est faux, on saute directement à Lfaux SANS évaluer « c < d ». // À VOUS : compléter le court-circuit pour sauter vers Lfaux dès le 1er faux. code3a = []; nEtiq = 0; function traduireEt(condG, condD, Lvrai, Lfaux) { // à compléter : si condG est faux -> Lfaux ; sinon tester condD ; // si condD est faux -> Lfaux ; sinon goto Lvrai. } const Lv = "Lvrai", Lf = "Lfaux"; traduireEt("a < b", "c < d", Lv, Lf); console.log("\na < b && c < d (court-circuit) :"); code3a.forEach((s) => console.log(" " + s));
Ce que la suite en fait
Le code intermédiaire est produit : expressions, affectations, contrôle, appels — tout est ramené à des instructions à trois adresses et à des sauts. Mais il reste abstrait sur un point majeur : où vivent les variables, et comment un appel de fonction s'organise réellement en mémoire.
Le bloc VI descend au niveau de la machine. Le chapitre 10 traite l'organisation mémoire — segments,
pile d'appels, enregistrements d'activation — c'est-à-dire où atterrissent les param et les
variables locales de ce chapitre. Le chapitre 11 produira enfin le code cible et l'optimisera.
À retenir
Vous avez parcouru les 8 sections.
Marquez-la terminée pour faire avancer votre parcours, ou revenez sur un point avant de passer à la suite.