Les différentes formes possibles de code intermédiaire sont
Un exemple représentatif:
x := y op z
où x,y, et z sont des noms de variables qui proviennent du texte source, ou des noms de variables temporaires générées par la compilation, et op est un opérateur quelconque.
| Catalogue des instructions à trois adresses | |
|---|---|
| Instruction | Sens |
| x := y op z | affectation et opération binaire |
| x := op y | affectation et opération unaire |
| y := y | affectation |
| goto l | branchement inconditionnelle, ou l est un étiquette |
| if x rel y goto l | branchement conditionnel, où rel est un opérateur relationnel binaire (<, >, = , etc). |
| param x call y, n | x est le nom d'un argument, y est le nom d'une fonction ou procédure, n est le nombre d'arguments. Ex. : |
| x := *y *x := y x:= &y | affectations avec des pointeurs |
| x := y[i] x[j] := y | affectation avec indices de tableaux |
Ces instructions à trois adresses constituent une forme abstraite du code intermédiaire. Classiquement, ces instructions sont représentées par une structure à plusieurs champs : quadruplets, triplets, triplets indirectes.
(op, arg1, arg2, cible). Lorsqu'il n'y a qu'un argument, ou pas d'argument, on met un blanc à la place. Idem pour l'opérateur dans le cas d'un branchement inconditionnel.
Exemple : A := -B*(C+D)
| Code à 3 adresses | Quadruplets | Triplets |
|---|---|---|
| T1 := -B | ( '-' , B, _, T1) | [0] ( '-', B, _ ) |
| T2 := C+D | ( '+', C, D, T2) | [1] ('+', C, D) |
| T3 := T1 * T2 | ('*', T1, T2, T3) | [2] ('*', [0], [1] ) |
| A := T3 | (':=', T3, _ ,A) | [3] (':=',[2], _ ) |
Un branchement conditionnel nécessite deux instruction triplets:
[0] (rel, x, y) [1] (ifgoto, [0], l )
Pour les variables (et constantes) de type entier, leur entrée dans la table des symbole doit comprendre un pointeur vers l'emplacement de stockage de la valeur. On y accèdera par E.ptr.
Posons une grammaire d'entier pour illustrer :
| S → id := E |
| E → E * E |
| E → E + E |
| E → -E |
| E → ( E ) |
| E → id |
Nous aurons besoin de deux fonctions pour ce travail, auxquelles nous ferons appel dans les actions de réduction :
Voici comment on les utilise pour réaliser des actions de façon schématique (on aurait de $1,$2, etc dans yacc/bison au lieu des non-terminaux :
à vérifier et finir ce tableau, ce n'est pas claire (pour moi, maintenant) pourquoi on adresse T directement et pas par T.ptr, sinon on risque de l'oublier et avoir des fuites de mémoire, je crains.
| N° | Règle | Action |
|---|---|---|
| 1 | S → id := E | { gencode( :=, E.ptr, id_ptr);} |
| 2 | E → E * E | { T = newtmp(); gencode('*', $1.ptr, $3.ptr, T); |
| 3 | E → E + E | |
| 4 | E → -E | |
| 5 | E → ( E ) | |
| 6 | E → id |
Gros exemple (page 4bis du 19/10) à rajouter.
Dans ce cas, il convient de vérifier la cohérence sémantique des types. Pour cela, nous regarderons E.type (ou E→type) pour connaitre le type de l'expression E.
id := E { if (id->type == E->type)
then gencode (':=', id->ptr, E->ptr)
else if (id->type==real && E->type ==int)
then {
T=newtmp();
gencode(':=', int2real(E->ptr), , T->ptr);
gencode(':=', T.ptr, , id->ptr);}
else error("affectation de types incompatibles");
}