English version
Accueil     À propos     Téléchargement     Ressources     Contactez-nous    

Déboguer un programme en Caml

Cette note présente rapidement deux techniques pour déboguer les programmes Caml :

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 :

  1. nous compilons le programme en mode debug :
    ocamlc -g uncaught.ml
    
  2. 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.