Cette note présente rapidement deux techniques pour déboguer les programmes Caml :
- Le traçage des appels de fonctions, qui fonctionne dans les toplevels de Caml Light et d'OCaml,
- Le débogueur d'OCaml qui permet d'analyser des programmes compilés avec ocamlc.
Tracer les appels de fonctions dans le toplevel
Le moyen le plus simple de déboguer les programmes dans le toplevel est de suivre les appels de fonctions en « traçant » la fonction suspecte :
let rec fib x = if x <= 1 then 1 else fib (x - 1) + fib (x - 2);; fib : int -> int = <fun> #trace fib;; fib is now traced. fib 3;; fib <-- 3 fib <-- 1 fib --> 1 fib <-- 2 fib <-- 0 fib --> 1 fib <-- 1 fib --> 1 fib --> 2 fib --> 3 - : int = 3 #untrace fib;; fib is no longer traced.
(Ce code concerne OCaml, en Caml Light, on écrit
trace "fib";;
à la place de #trace fib;;
et
untrace "fib";;
à la place de #untrace fib;;
.)
Fonctions polymorphes
Une difficulté avec les fonctions polymorphes vient du fait que la trace n'est pas très informative pour les arguments et/ou les résultats polymorphes. Considérons par exemple une fonction de tri à bulle :
let exchange i j v = let aux = v.(i) in v.(i) <- v.(j); v.(j) <- aux;; exchange : int -> int -> 'a vect -> unit = <fun> let one_pass_vect fin v = for j = 1 to fin do if v.(j - 1) > v.(j) then exchange (j - 1) j v done;; one_pass_vect : int -> 'a vect -> unit = <fun> let bubble_sort_vect v = for i = vect_length v - 1 downto 0 do one_pass_vect i v done;; bubble_sort_vect : 'a vect -> unit = <fun> #trace one_pass_vect;; one_pass_vect is now traced. let q = [| 18; 3; 1 |];; q : int vect = [|18; 3; 1|] bubble_sort_vect q;; one_pass_vect <-- 2 one_pass_vect --> <fun> one_pass_vect* <-- [|<poly>; <poly>; <poly>|] one_pass_vect* --> () one_pass_vect <-- 1 one_pass_vect --> <fun> one_pass_vect* <-- [|<poly>; <poly>; <poly>|] one_pass_vect* --> () one_pass_vect <-- 0 one_pass_vect --> <fun> one_pass_vect* <-- [|<poly>; <poly>; <poly>|] one_pass_vect* --> () - : unit = ()
La fonction one_pas_vect
étant polymorphe son
vecteur argument est imprimé comme un vecteur de valeurs
polymorphes, [|<poly>; <poly>; <poly>|]
, et l'on ne suit pas l'évolution des
calculs.
La solution simple à ce problème est de travailler avec une version monomorphe de la fonction à corriger. On obtient facilement cette version monomorphe traçable, en utilisant une contrainte de type. En général, cela suffit à comprendre et à corriger l'erreur qui s'était glissée dans la fonction polymorphe. Il suffit ensuite de supprimer la contrainte de type pour revenir à une version polymorphe de la fonction maintenant corrigée. Dans le cas du tri, il est même possible que le caractère polymorphe obtenu ne soit pas un desiderata conscient du programmeur, mais une conséquence insoupçonnée des comparaisons polymorphes de Caml !
let exchange i j (v : int vect) = [...] exchange : int -> int -> int vect -> unit = <fun> [...] one_pass_vect : int -> int vect -> unit = <fun> [...] bubble_sort_vect : int vect -> unit = <fun> #trace one_pass_vect;; one_pass_vect is now traced. let q = [| 18; 3; 1 |];; q : int vect = [|18; 3; 1|] bubble_sort_vect q;; one_pass_vect <-- 2 one_pass_vect --> <fun> one_pass_vect* <-- [|18; 3; 1|] one_pass_vect* --> () one_pass_vect <-- 1 one_pass_vect --> <fun> one_pass_vect* <-- [|3; 1; 18|] one_pass_vect* --> () one_pass_vect <-- 0 one_pass_vect --> <fun> one_pass_vect* <-- [|1; 3; 18|] one_pass_vect* --> () - : unit = ()
Limitations
Pour suivre l'évolution des variables mutables d'un programme la trace ne suffit pas, il faut un mécanisme supplémentaire qui permet d'arrêter le programme en cours d'exécution et d'interroger son état interne, c'est le correcteur symbolique avec son mode pas à pas.
L'exécution en mode pas à pas d'un programme fonctionnel a un sens un peu délicat à définir et à comprendre. On fait intervenir des événements d'exécution qui se produisent par exemple au passage d'un paramètre ou à l'entrée d'un filtrage, ou encore à la sélection d'une clause de filtrage. La progression de l'exécution est comptée par ces évènements, indépendamment des instructions exécutées par la machine.
Bien que cela soit difficile à implémenter, il existe un tel déboguer pour OCaml sous Unix : ocamldebug (il en existe également un pour Caml Light, sous la forme d'une contribution d'un utilisateur). Son utilisation est illustrée dans la section suivante.
En fait l'expérience prouve que pour les programmes complexes,
on corrige ses programmes en utilisant l'impression explicite (à
l'aide de printf
),
car cela permet de n'afficher que les données pertinentes et de
façon bien plus compacte et adaptée qu'un imprimeur
générique.
Le débogueur d'OCaml
Voici maintenant un rapide tutoriel pour le débogueur d'OCaml (ocamldebug). Avant de commencer, veuillez notez qu'ocamldebug n'es pas présent dans les ports Windows natifs d'OCaml (mais il fonctionne sous le port Cygwin).
Lancement du debogueur
Considérons le code trivialement faux suivant suivant, écrit dans le fichier uncaught.ml :
(* file uncaught.ml *) let l = ref [];; let find_address name = List.assoc name !l;; let add_address name address = l := (name, address) :: ! l;; add_address "IRIA" "Rocquencourt";; print_string (find_address "INRIA"); print_newline ();;
À l'éxécution, le programme produit une exception non-rattrapée
Not_found
. Pour trouver ou
et pourquoi cette exception a été levée, nous pouvez procéder
comme suit :
-
nous compilons le programme en mode debug :
ocamlc -g uncaught.ml
-
nous lançons le débogueur
ocamldebug a.out
Le débogueur répond alors par une bannière et une invite :
OCaml Debugger version 3.12.1 (ocd)
Trouver la cause de l'exception
Tapez r (pour run); vous obtenez
(ocd) r Loading program... done. Time : 12 Program end. Uncaught exception: Not_found (ocd)
Cela s'explique de lui même, n'est-ce pas ? Ainsi, il vous faut reculer dans l'exécution du program et positionner le compteur de programme avant l'instant où l'exception est levée. Il suffit pour cela de taper b comme backstep, et vous obtenez
(ocd) b Time : 11 - pc : 15500 - module List 143 [] -> <|b|>raise Not_found
Le débogeur vous indique que vous êtes dans le module
List
, dans un filtrage sur une liste qui a déjà choisi
le cas []
et s'apprête à exécuter raise Not_found
puisque le programme est arrêté juste avant cette
expression (comme indiqué par la marque
<|b|>
.
Cependant, vous voulez que le débogueur vous indique quelle
procédure a appelé celle du module List
, et également
qui a appelé cette procédure. Il vous faut pour cela explorer la
pile d'exécution, en entrant bt (comme
backtrace) :
(ocd) bt #0 Pc : 15500 List char 3562 #1 Pc : 19128 Uncaught char 221
Ainsi, la dernière fonction appellée est celle du module
List
qui se situe au caractère 3562, c'est à
dire :
let rec assoc x = function [] -> raise Not_found ^ | (a,b)::l -> if a = x then b else assoc x l
La fonction qui a appelé List.assoc
est dans le
module Uncaught
, dans le fichier uncaught.ml
au caractère 221 :
print_string (find_address "INRIA"); print_newline ();; ^
Pour résumer : lorsque vous développez un programme, vous
pouvez le compiler avec l'option -g, de manière à pouvoir
le déboguer. Pour trouver une exception suspecte, il vous suffit
de taper ocamldebug a.out
, puis r, b,
et bt pour obtenir la trace d'excécution.
Obtenir de l'aide et des informations dans le débogueur
Pour obtenir plus d'informations sur l'état courant du
débogueur, vous pouvez utiliser le commande
info. La commande help
permet d'obtenir de
l'aide :
(ocd) info breakpoints No breakpoint. (ocd) help break 1 15396 in List, character 3539 break : Set breakpoint at specified line or function. Syntax: break function-name break @ [module] linenum break @ [module] # characternum
Dispoer des points d'arrêt
Définissons un point d'arrête, puis recommenons l'exécution du
proramme depuis le début ((g)oto 0
puis
(r)un
) :
(ocd) break @Uncaught 9 Breakpoint 3 at 19112 : file Uncaught, line 9 column 34 (ocd) g 0 Time : 0 Beginning of program. (ocd) r Time : 6 - pc : 19112 - module Uncaught Breakpoint : 1 9 add "IRIA" "Rocquencourt"<|a|>;;
Nous pouvonz alors avancer pas à pas et voir ce qui se passe
lorsque find_address
est sur le point d'être
appelée :
(ocd) s Time : 7 - pc : 19012 - module Uncaught 5 let find_address name = <|b|>List.assoc name !l;; (ocd) p name name : string = "INRIA" (ocd) p !l $1 : (string * string) list = ["IRIA", "Rocquencourt"] (ocd)
Nous pouvonz maintenant comprendre pourquoi
List.assoc
va échouer.
Utiliser let débogueur sous (X)Emacs
On peut appeler le débogueur sous Emacs en tapant ESC-x puis camldebug a.out. Emacs vous montrera alors directement le fichier et la position donnée par la débogueur. Vous pouvez avancer et reculer dans l'exécution du programme avec ESC-b et ESC-s, et placer des points d'arrêts avec CTRL-X space.