Les deux premières parties de cet examen traitent de l'ajout de diverses constructions au langage accepté par le compilateur zyva du cours. Les deux parties du problème sont indépendantes et, au sein de chaque partie, les questions sont de difficulté croissante. Une troisième partie propose quelques questions indépendantes de toutes les autres.
Dans l'instruction conditionnelle, if expression
then instruction else instruction,
il est pratique de rendre optionnelle la partie else
instruction. Cela permet par exemple d'écrire la fonction
valeur absolue ainsi :
function abs(x : integer) : integer ; begin if x < 0 then x := -x ; abs := x end ; |
Question 1. On modifie l'analyseur syntaxique de zyva en insérant la ligne
| IF expression THEN instruction {…} |
entre les lignes 81 et 82 du fichier parser.mly. Donner l'action de la nouvelle règle de l'analyseur (les … ci-dessus). Pour le type des arbres de syntaxe abstraite, voir le fichier pp.mli.
| IF expression THEN instruction { If ($2, $4, Sequence []) } |
Question 2. Voici un compte-rendu de la compilation de l'analyseur modifié :
# ocamlyacc parser.mly 1 shift/reduce conflict.
Et voici l'extrait pertinent du fichier de diagnostics de ocamlyacc :
124: shift/reduce conflict (shift 138, reduce 22) on ELSE state 124 instruction : IF expression THEN instruction . ELSE instruction (21) instruction : IF expression THEN instruction . (22) ELSE shift 138 SEMI reduce 22 END reduce 22
a) Démontrer que la nouvelle grammaire est ambiguë en exhibant
une instruction pouvant être interprétée de deux façons.
b) Expliciter l'interprétation effectuée par l'analyseur
shift/reduce produit par ocamlyacc (sachant qu'entre shift et
reduce, ocamlyacc choisit shift).
if E1 then if E2 then I1 else I2 |
if E1 then begin if E2 then I1 end else I2 |
if E1 then begin if E2 then I1 else I2 end |
Question 3. Réécrire la grammaire de sorte qu'elle ne soit plus ambiguë et produise les mêmes arbres de syntaxe abstraite que l'analyseur ocamlyacc lors de la question précédente. Seules sont demandées les règles de la nouvelle grammaire, on ne donnera pas les actions. Indication : On peut lever l'ambiguïté en divisant le non-terminal instruction en deux non-terminaux.
instruction: fermé | ouvert ; |
Les nouveaux non-terminaux sont tels que le non-terminal fermé ne produit jamais de mot se terminant par THEN instruction, et que le non-terminal ouvert produit ces mots.
instruction: fermé | ouvert ; fermé: IF expression THEN fermé ELSE fermé | WHILE expression DO fermé /* Règles reprises de la définition de instruction du parser.mly original */ | IDENT COLONEQUAL expression | BEGIN bloc END | READ LPAREN IDENT RPAREN | WRITE LPAREN expression RPAREN | WRITELN LPAREN expression RPAREN | array_expression LBRACKET expression RBRACKET COLONEQUAL expression ; ouvert: IF expression THEN instruction | IF expression THEN fermé ELSE ouvert | WHILE expression DO ouvert ; |
On se propose maintenant d'effectuer les retours de fonction et
de procédure à l'aide de l'instruction return, comme en Java ou
en C.
L'instruction return permet par exemple une écriture assez naturelle de la fonction suivante de vérification que tous les éléments d'un tableau sont non-nuls.
1 function tousNonNuls (t:array of integer ; n:integer):boolean ; 2 var i:integer ; 3 begin 4 i := 0 ; 5 while i < n do begin 6 if t[i] = 0 then 7 return false 8 else 9 i := i+1 10 end ; 11 return true 12 end ; |
La sémantique informelle de l'instruction « return » est la suivante.
Une fonction transmet son résultat au programme appelant par l'instruction « return expression ». L'execution de cette instruction entraîne l'évaluation de l'expression, puis le retour au programme appelant.Une procédure retourne au programme appelant par l'instruction « return » (sans argument). Aucune valeur n'est transmise.
On notera donc que la fonction tousNonNuls
ne parcourt le tableau t
que jusqu'à trouver son premier élément nul.
Pour rendre compte de cette nouvelle instruction, la définition des arbres de syntaxe abstraite est étendue par l'ajout de la ligne suivante à la fin du fichier pp.mli.
| Return of expression option |
Note : Dans toute cette partie on suppose que les programmes pseudo-pascal sont bien typés, en particulier le corps d'une fonction ne contient pas « return » sans argument et le corps d'une procédure ne contient pas « return expression ».
Question 4. Modifier les analyseurs lexical et syntaxique de zyva (fichiers lexer.mll et parser.mly, donnés en annexe) pour intégrer cette nouvelle instruction.
%token RETURN |
| RETURN { Return None } | RETURN expression { Return (Some $2) } |
keywords
(par exemple après la ligne 10).
add_keyword "return" RETURN ; |
Question 5. L'existence d'une instruction explicite pour retourner de la fonction en cours met en évidence une erreur de programmation : la fin du corps de la fonction est atteinte sans qu'aucune valeur ne soit renvoyée. Une telle erreur est par exemple possible si on modifie la fonction tousNonNuls en supprimant le ligne numéro 11.
Écrire (en Caml) une fonction retourneToujours qui prend en
argument le corps d'une fonction et qui renvoie
un booléen. Le booléen vaudra true
si l'on a pu détecter
que l'exécution du corps se termine nécessairement
par return expression (quand elle termine),
et false
autrement.
val retourneToujours : Pp.instruction -> bool |
Attention :
Une fonction retourneToujours
qui répond toujours false
est conforme à la spécification demandée. Toutefois, il est clair
qu'une telle fonction ne fait aucun effort et ne constitue pas une
réponse valable.
Note : Ceux qui ne se sentent pas à l'aise en Caml peuvent répondre autrement, en définissant (par induction) un prédicat sur les instructions.
Return
a bien un argument Some
e.
let rec retourneToujours i = match i with | Return _ -> true | Sequence (i::rem) -> retourneToujours i || retourneToujours (Sequence rem) | Sequence [] -> false | If (_, inst, insf) -> retourneToujours inst && retourneToujours insf | _ -> false |
Question 6. Modifier la compilation vers le code intermédiaire pour qu'elle traite la nouvelle instruction return.
On modifiera la seule fonction cinstruction
en supposant qu'elle est
maintenant de la forme :
(* Le nouveau type de cinstruction est : (access, Frame.frame) Env.environment -> Frame.frame -> Pp.instruction -> Code.stm *) let rec cinstruction env f i = match i with … |
Où f
est le frame de la fonction (ou procédure) en cours de
compilation.
Indications : Vous disposez en annexe de la définition du code intermédiaire code.mli, du source trans.ml du compilateur vers le code intermédiaire, et de l'interface frame.mli du module Frame, dont voici un extrait utile.
val frame_result : frame -> temp option (* Renvoie le temporaire choisi pour retourner le résultat d'une vraie fonction, None pour une procédure *) val frame_return : frame -> label (* Renvoie l'étiquette choisie pour l'épilogue (fin de la sous-routine) *) |
cinstruction
, je n'ai
indiqué que les cas du filtrage de l'instruction i
qui changent :
let rec cinstruction env f i = match i with | Sequence is -> (* Comme avant, sauf qu'il faut ajouter l'argument f après env *) Seq (List.map (cinstruction env f) is) | If (e, inst, insf) -> (* Idem *) | While (e, i) -> (* Idem *) | Procedure_call … | Seti … (* Pas de changement *) (* Seuls cas réellement nouveaux *) | Return None -> Jump (Frame.frame_return f) | Return (Some e) -> let ce = cexpr env e in let tr = match Frame.frame_result f with | Some t -> t | None -> assert false in Seq [Move_temp (tr, ce) ; Jump (Frame.frame_return f)] |
Question 7. Cette question aborde deux points complémentaires de la
question 5. Les réponses peuvent être relativement brèves (mais précises).
a)
Cela a-t-il un sens de procéder à une analyse du corps des procédure,
comme la question 5 procédait à une analyse du corps des fonctions ?
b) Indiquer comment résoudre la question 5 à
l'aide d'une analyse de durée de vie pratiquée sur le code
intermédiaire.
Question 8. Soit f une fonction quelconque de pseudo-pascal.
Dans le corps de f, l'instruction « return f(…) »
est un appel récursif terminal, dont la compilation peut être
largement optimisée.
Grossièrement, l'optimisation consiste à remplacer l'appel terminal
par un saut vers une cible bien choisie dans le corps de f.
a) Identifier précisément la « cible bien
choisie ». On supposera par la suite que l'étiquette définissant cette
cible est en place dans le code intermédiaire (de la fonction en cours
de compilation) et accessible à l'aide
d'une nouvelle fonction frame_bien_choisie
du
module Frame.
b) Donner un code intermédiaire que peut
produire la compilation
optimisant l'appel récursif terminal de cette fonction pseudo-pascal.
function fact(n,r:integer):integer ; begin if n = 0 then return r else return fact(n-1, n*r) end ; |
Attention : Il faut respecter la sémantique : le programme
optimisé doit produire le même résultat que le programme non-optimisé.
Note :
Les temporaires contenant les arguments seront notés tn et tr,
le temporaire contenant le résultat sera noté tf.
Vous pouvez donner le code intermédiaire dans le style adopté dans la
section 7.3 du poly.
c) Modifier la fonction cinstruction
de la
question 6 pour qu'elle
procède à l'optimisation des appels récursifs terminaux.
Lenter: Cjump L15 L16 (= tn 0) L16: (set t'n (- tn 1)) (set t'r (* tn tr)) (set tn t'n) (set tr t'r) Jump Lenter L15: (set tf tr)Où (set t e) est l'instruction
Move_temp
.
On note l'emploi de temporaires additionnels t'n et t'r.cinstruction
on
ajoute cette clause avant la clause traitant le motif
Return (Some e)
.
| Return (Some (Function_call (g, args))) when Env.find_definition env g == f -> let ts = List.map (fun _ -> Gen.new_temp ()) args in let cargs = List.map2 (fun t e -> Move_temp (t, cexpr env e)) ts args and cmove_params = List.map2 (fun at t -> Move_temp (at, Temp t)) (Frame.frame_args f) ts in Seq [Seq cargs ; Seq cmove_params ; Jump (Frame.frame_bien_choisie f)] |
ts
permet de calculer les nouvelles
valeurs des arguments à l'aide des anciennes, sans eux, le code généré
pourrait être incorrect, si par exemple le second argument de l'appel
terminal fait référence à la valeur du premier argument. Comme dans
l'exemple donné.
Cette partie pose quelques questions assez diverses. En y répondant
brièvement, vous pouvez faire état de votre compréhension du sujet de
la compilation.
Question 9. En dehors des bienfaits que le contrôle de type apporte au programmeur, expliquer pourquoi les compilateurs comportent presque toujours une phase de contrôle des types.
Question 10. Dans le cas d'une compilation vers le MIPS, quel nombre minimum de registres peut-on autoriser l'allocation de registres à employer ? Quels registres choisiriez vous et quelles conventions d'appel (la cible reste le MIPS), afin de mimimiser les modifications à apporter à zyva ?
Question 11. Expliquer l'intérêt (marginal) qu'il peut y avoir à rendre, lors de la selection, l'instruction du code intermédiaire :
par l'instruction MIPS move t1, t2 au lieu de l'instruction add t1, t2, 0.
Ce document a été traduit de LATEX par HEVEA