Outils de mise au point
Il y a deux outils de mise au point. Le premier
outil est un mécanisme de trace utilisable, sur les fonctions
globales, dans la boucle d'interaction. Le second outil est un
debug sur les programmes hors boucle d'interaction.
Suite à une première exécution, il permet de revenir sur des points
d'arrêt, d'explorer des valeurs ou de relancer certaines fonctions
avec de nouveaux paramètres. Ce second outil
n'est utilisable que sous Unix car il duplique le processus
programme via un fork (voir page ??).
Trace
On appelle trace d'une fonction l'affichage de la valeur de ses
paramètres d'appels dans le cours d'un programme ainsi que celui de
son résultat.
Les commandes de traçage d'exécution sont des directives de la boucle
d'interaction. Elles permettent de tracer une fonction, d'arrêter sa
trace ou d'arrêter toutes les traces posées. Ces trois directives sont
données dans le tableau ci-dessous.
#trace nom |
trace la fonction nom |
#untrace nom |
arrête de tracer la fonction nom |
#untrace_all |
annule toutes les traces |
Voici un premier exemple où on définit une fonction f :
# let
f
x
=
x
+
1
;;
val f : int -> int = <fun>
# f
4
;;
- : int = 5
On indique maintenant que cette fonction est tracée, c'est-à-dire
que le passage d'un argument ou le retour d'un résultat seront affichés.
#
#trace
f;;
f is now traced.
# f
4
;;
f <-- 4
f --> 5
- : int = 5
Le passage de l'argument 4 à f est affiché, la
fonction f effectue le calcul demandé et le résultat retourné
est aussi affiché. Les (valeurs) paramètres d'appels sont indiqués
par une flèche vers la gauche et la valeur de retour, par une
flèche vers la droite.
Fonctions à plusieurs arguments
Les fonctions à plusieurs arguments (ou retournant une fermeture) sont
aussi traçables. Chaque passage d'un argument est affiché. Pour
distinguer les différentes fermetures, on note avec une * le
nombre d'arguments déjà passés à la fermeture. Soit la fonction
verif_div qui prend 4 nombres (a, b, q, r) correspondant à
la division entière : a = bq + r.
# let
verif_div
a
b
q
r
=
a
=
b*
q
+
r;;
val verif_div : int -> int -> int -> int -> bool = <fun>
# verif_div
1
1
5
2
1
;;
- : bool = true
Sa trace montrera le passage des 4 arguments :
#
#trace
verif_div;;
verif_div is now traced.
# verif_div
1
1
5
2
1
;;
verif_div <-- 11
verif_div --> <fun>
verif_div* <-- 5
verif_div* --> <fun>
verif_div** <-- 2
verif_div** --> <fun>
verif_div*** <-- 1
verif_div*** --> true
- : bool = true
Fonctions récursives
La trace apporte de précieuses informations sur les fonctions
récursives. Elle permet de détecter aisément un mauvais critère
d'arrêt.
On définit la fonction appartient qui teste si un entier
appartient à une liste d'entiers de la manière suivante :
# let
rec
appartient
(e
:
int)
l
=
match
l
with
[]
->
false
|
t::q
->
(e
=
t)
||
appartient
e
q
;;
val appartient : int -> int list -> bool = <fun>
# appartient
4
[
3
;5
;7
]
;;
- : bool = false
# appartient
4
[
1
;
2
;
3
;
4
;
5
;
6
;
7
;
8
]
;;
- : bool = true
La trace de l'appel de appartient 4 [3;5;7] affichera les
quatre appels de cette fonction et les résultats retournés.
#
#trace
appartient
;;
appartient is now traced.
# appartient
4
[
3
;5
;7
]
;;
appartient <-- 4
appartient --> <fun>
appartient* <-- [3; 5; 7]
appartient <-- 4
appartient --> <fun>
appartient* <-- [5; 7]
appartient <-- 4
appartient --> <fun>
appartient* <-- [7]
appartient <-- 4
appartient --> <fun>
appartient* <-- []
appartient* --> false
appartient* --> false
appartient* --> false
appartient* --> false
- : bool = false
À chaque appel de la fonction appartient l'argument 4
puis la liste dans laquelle vérifier sont passés. Quand la liste
devient vide, alors la fonction retourne la valeur false qui est
propagée comme valeur de retour de chaque appel récursif en attente.
L'exemple suivant montre le parcours partiel
de la liste lorsque l'élément demandé y apparaît :
# appartient
4
[
1
;
2
;
3
;
4
;
5
;
6
;
7
;
8
]
;;
appartient <-- 4
appartient --> <fun>
appartient* <-- [1; 2; 3; 4; 5; 6; 7; 8]
appartient <-- 4
appartient --> <fun>
appartient* <-- [2; 3; 4; 5; 6; 7; 8]
appartient <-- 4
appartient --> <fun>
appartient* <-- [3; 4; 5; 6; 7; 8]
appartient <-- 4
appartient --> <fun>
appartient* <-- [4; 5; 6; 7; 8]
appartient* --> true
appartient* --> true
appartient* --> true
appartient* --> true
- : bool = true
Dès que 4
est tête de liste, la fonction retourne la valeur
true qui est propagée par les appels récursifs en attente.
Si la fonction appartient avait inversé l'ordre du
||, elle retournerait toujours le bon résultat, mais
effectuerait dans tous les cas le parcours complet de la liste.
# let
rec
appartient
(e
:
int)
=
function
[]
->
false
|
t::q
->
appartient
e
q
||
(e
=
t)
;;
val appartient : int -> int list -> bool = <fun>
# #trace
appartient
;;
appartient is now traced.
# appartient
3
[
3
;5
;7
]
;;
appartient <-- 3
appartient --> <fun>
appartient* <-- [3; 5; 7]
appartient <-- 3
appartient --> <fun>
appartient* <-- [5; 7]
appartient <-- 3
appartient --> <fun>
appartient* <-- [7]
appartient <-- 3
appartient --> <fun>
appartient* <-- []
appartient* --> false
appartient* --> false
appartient* --> false
appartient* --> true
- : bool = true
Bien que 3
soit le premier élément de la liste, celle-ci
est entièrement parcourue. Ainsi, le mécanisme de trace permet
également une analyse d'efficacité des fonctions récursives.
Fonctions polymorphes
La trace d'arguments d'un type paramétré n'affichera pas la valeur
correspondant au paramètre de type. Par exemple si la fonction
appartient avait été écrite sans contrainte explicite de type :
# let
rec
appartient
e
l
=
match
l
with
[]
->
false
|
t::q
->
(e
=
t)
||
appartient
e
q
;;
val appartient : 'a -> 'a list -> bool = <fun>
La fonction appartient est maintenant d'un type polymorphe.
Sa trace n'affiche plus la valeur des arguments, mais les remplace
par l'indication (poly).
#
#trace
appartient
;;
appartient is now traced.
# appartient
3
[
2
;3
;4
]
;;
appartient <-- <poly>
appartient --> <fun>
appartient* <-- [<poly>; <poly>; <poly>]
appartient <-- <poly>
appartient --> <fun>
appartient* <-- [<poly>; <poly>]
appartient* --> true
appartient* --> true
- : bool = true
La boucle d'interaction d'Objective CAML ne sait afficher que les valeurs de
type monomorphe. De plus, il ne conserve que le type inféré des
déclarations globales. Ainsi, après compilation de l'expression
appartient 3 [2;3;4], la boucle d'interaction ne dispose plus, en
fait d'information de type, que du type 'a -> 'a list -> bool
de la fonction appartient. Les types (monomorphes) de
3 et [2;3;4] ont été perdus car les valeurs ne
conservent pas d'information de type : c'est le typage statique. C'est
pourquoi, le mécanisme de trace attribue aux arguments
de la fonction appartient les types polymorphes 'a
et 'a list dont il n'affiche pas les valeurs.
C'est l'absence d'information de type dans les valeurs qui entraîne
l'impossibilité de construire une fonction print générique de
type 'a -> unit.
Fonctions locales
Les fonctions locales ne sont pas traçables pour les mêmes raisons
liées au typage statique. Seuls les types des déclarations
globales sont conservés dans l'environnement de la boucle d'interaction.
Pourtant le style de programmation suivant est courant :
# let
appartient
e
l
=
let
rec
app_aux
l
=
match
l
with
[]
->
false
|
t::q
->
(e
=
t)
||
(app_aux
q)
in
app_aux
l;;
val appartient : 'a -> 'a list -> bool = <fun>
La fonction globale ne fait qu'appeler une fonction locale effectuant
la partie intéressante du travail.
Remarques sur la trace
Le mécanisme de trace est actuellement l'unique outil de mise au point
multi-plate-formes. Ses deux faiblesses sont l'absence de trace des
déclarations locales et le non affichage des paramètres polymorphes
des fonctions. Cela restreint fortement son usage, principalement aux
premiers pas avec le langage.
Debug
L'outil de mise au point des programmes, ocamldebug, est un
débogueur au sens usuel du terme. Il permet l'exécution
pas-à-pas, l'insertion de points d'arrêt, l'exploration et la
modification des valeurs de l'environnement.
Exécuter pas-à-pas un programme suppose que l'on sache
déterminer ce qu'est un pas de programme. En programmation
impérative, cette notion est assez simple : grosso modo, un pas
de programme est une instruction du langage. Mais cette définition
n'a plus grand sens en programmation fonctionnelle; on
parle plutôt d'événement (event) de
programmes. Ceux-ci seront une application, l'entrée dans une
fonction, un filtrage, une alternative, une boucle, un élément de
séquence, etc.
Warning
Cet outil ne fonctionne que sous Unix.
Compilation en mode debug
L'option de compilation -g engendre un fichier .cmo
permettant d'engendrer les instructions nécessaires au debug.
Cette option n'est connue que du compilateur de code-octet. Il
sera nécessaire d'indiquer cette option lors de la compilation des
différents fichiers d'une application. Une fois un exécutable produit,
son exécution en mode debug s'effectue par la commande
ocamldebug de la manière suivante :
On prend l'exemple suivant du fichier fact.ml de calcul de la
fonction factorielle :
let
fact
n
=
let
rec
fact_aux
p
q
n
=
if
n
=
0
then
p
else
fact_aux
(p+
q)
p
(n-
1
)
in
fact_aux
1
1
n;;
Le programme principal du fichier main.ml
part pour une longue récursion suite à l'appel de Fact.fact à
-1.
let
x
=
ref
4
;;
let
go
()
=
x
:=
-
1
;
Fact.fact
!
x;;
go();;
On compile les deux fichiers avec l'option -g :
$ ocamlc -g -i -o fact.exe fact.ml main.ml
val fact : int -> int
val x : int ref
val go : unit -> int
Lancement du debug
Une fois un exécutable crée en mode debug, il est possible de démarrer
l'exécution dans ce mode.
$ ocamldebug fact.exe
Objective Caml Debugger version 2.04
(ocd)
Suivi du contrôle d'exécution
Le contrôle d'exécution passe par les événements du programme. On peut
soit avancer ou reculer de n éléments de programme, soit avancer ou
reculer jusqu'au point d'arrêt le plus proche (ou de n points
d'arrêt). On indique un point d'arrêt sur une fonction ou sur un
élément de programme. Le choix de l'élément de langage est donné par
les coordonnées en ligne, colonne ou numéro de caractères. Ces
localisations peuvent être relatives à un module.
Dans l'exemple ci-dessous, on pose un point d'arrêt sur la
quatrième ligne du module Main :
(ocd) step 0
Loading program... done.
Time : 0
Beginning of program.
(ocd) break @ Main 4
Breakpoint 1 at 4964 : file Main, line 4 column 3
Les initialisations du module se font avant le programme proprement
dit. C'est pour cette raison que le point d'arrêt à la ligne 4 ne se
rencontre qu'après 4964 instructions.
On avance ou recule dans l'exécution soit par rapport aux éléments de
programme, soit par rapport aux points d'arrêt. run et reverse exécutent jusqu'au point d'arrêt le plus proche, le premier
dans le sens de l'exécution, le second en marche arrière. Le
commande step avance de 1 ou n éléments de programme en
entrant dans les fonctions, next n'entre pas dans les appels de
fonctions. Respectivement backstep et previous effectuent
les mêmes opérations en marche arrière. Enfin finish termine
l'appel de la fonction courante alors que start retourne à
l'élément de programme précédant l'invocation de la fonction.
Pour suivre notre exemple, on avance jusqu'au point d'arrêt puis on
exécute trois événements de programme :
(ocd) run
Time : 6 - pc : 4964 - module Main
Breakpoint : 1
4 <|b|>Fact.fact !x;;
(ocd) step
Time : 7 - pc : 4860 - module Fact
2 <|b|>let rec fact_aux p q n =
(ocd) step
Time : 8 - pc : 4876 - module Fact
6 <|b|>fact_aux 1 1 n;;
(ocd) step
Time : 9 - pc : 4788 - module Fact
3 <|b|>if n = 0 then p
Exploration de valeurs
Au niveau d'un arrêt, il est possible d'afficher les valeurs associées
aux variables du bloc d'activation. Les commandes print et display affichent selon des profondeurs différentes les valeurs
associées à une valeur.
Nous allons afficher la valeur de n, puis remonter de trois
pas pour afficher le contenu de x :
(ocd) print n
n : int = -1
(ocd) backstep 3
Time : 6 - pc : 4964 - module Main
Breakpoint : 1
4 <|b|>Fact.fact !x;;
(ocd) print x
x : int ref = {contents=-1}
Les accès aux champs d'un enregistrement ou via
l'indice d'un tableau sont acceptés par les commandes d'affichage.
(ocd) print x.contents
$1 : int = -1
Pile d'exécution
La pile d'exécution permet de visualiser l'imbrication des appels de
fonctions. La commande backtrace ou bt montre
l'empilement des appels. Les commandes up et down
sélectionnent le bloc d'activation suivant ou précédant. Enfin la
commande frame décrit le bloc courant.