Précédent Index Suivant

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 11 5 2 1;;
- : bool = true


Sa trace montrera le passage des 4 arguments :

# #trace verif_div;;
verif_div is now traced.
# verif_div 11 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.


Précédent Index Suivant