Notes de la séance 4 de TransProg M2
====================================

Suite du travail sur Fopix2Javix : Compilation directe de Fopix vers Javix

Exemples complets : fact et factopt

Voir au tableau pendant la séance.

Appels récursifs terminaux (tail calls)

Si le code de la fonction f se termine par un appel à une autre
fonction g (qui peut être f de nouveau en cas de récursion),
pas besoin de sauvegarder des variables, ni de placer une adresse
de retour sur la pile : on peut s’arranger pour recycler l’adresse
de retour de l’appel en cours à f lors du lancement de g !
C’est l’optimisation des appels terminaux. Voir par exemple factopt.fx.
Attention, le tout premier appel de fonction ne peut être optimisé
(pas encore d’adresse de retour). Pour Fopix2Javix, cette optimisation
n’est pas obligatoire (mais recommandé). De plus, ne pas chercher pour
l’instant à changer le code que l’on compile pour le rendre récursif
terminal.

Appel de fonction indirect

Il s’agit des Call (e, ...) où l’expression e indiquant la
fonction à appeler n’est pas directement un Fun(f). Par exemple e
peut être un Var(...) (cf. les exemples listmap.fx ou compose.fx).
Ou bien encore un If(...,Fun(f),Fun(g)) (ok, exemple assez
artificiel, mais pourquoi pas). Ou même e peut lui-même être
un Call(...) (en programmation fonctionnel, un appel de fonction
peut retourner une fonction).

En tout cas, e doit être une expression qui après calcul donne
un nom de fonction (cf. RFun dans FopixInterp). Mais un
compilateur ne doit pas faire ce calcul pour savoir quelle fonction
lancer, ce n’est pas son rôle. Le compilateur met juste tout en place
pour que le calcul se déroule lorsque l’utilisateur lancera le .class.

Rappel sur la syntaxe concrète Fopix :

Bref, f(x,y) est juste une syntaxe courte et pratique pour la forme
générale ?(&f)(x,y), les deux donnant l’AST Call(Fun(f),...).

Avec un appel de fonction “direct”, on connait à compile-time la
fonction à déclencher, donc un simple Goto f convient pour
déclencher cette fonction. Rappel des différentes phases d’un
Call(Fun(f),args) (sans optimisation de tail-call) :

;; (1) sauvegarde des variables dans la pile
;; (2) calcul des args
;; (3) stockage des args
;; (4) Push code du label_retour_123
Goto f
label_retour_123:
;; (5) restauration des variables

En fait, on peut décaler certaines étapes si on le souhaite, par
exemple décaler (4) avant (2), pour uniformiser avec ce que l’on
va faire maintenant.

Pour un appel indirect, il nous fait un saut “dynamique”, dont la
destination est décidée à run-time. On utilise pour cela la même
idée du Tableswitch final (après le label dispatch:), et la
même idée d’une correspondance entre des labels et des entiers
servant de code pour ces labels. Simplement, au lieu de faire
cela juste pour les labels de retour comme auparavant, on fait
cela également pour les labels de fonctions (au moins ceux pour
lesquels un Fun(f) est présent quelque-part dans le code sans
être immédiatement en tête d’un Call( )). Un tel Fun(f) isolé
est alors à compiler en un Push du numéro choisi correspondant
au label de f suivi d’un Box. Et pour un Call(e,args) avec
e quelconque (sans optimisation de tail-calls), voici l’ordre
des phases que je suggère :

;; (1) sauvegarde des variables dans la pile
;; (2) Push code du label_retour_123
;; (3) compilation de l'expression e
Unbox (du code correspondant à e)
;; (4) calcul des args
;; (5) stockage des args
Goto dispatch
label_retour_123:
;; (6) restauration des variables

Là encore, cet ordre n’est pas le seul possible, vous pouvez l’adapter
selon vos goûts tant que l’on obtient le même comportement, et en
particulier que les sous-expressions e et les args sont compilés
avec les bonnes variables encore en place.