The trace facility in Caml

Contact the author Pierre.Weis@inria.fr

Created in December 1995.

Table of contents:

How to debug programs ?

The simplest way to debug programs is to follow the function calls, by ``tracing'' the faulty function:

#let rec fib x = if x <= 1 then 1 else fib (x - 1) + fib (x - 2);;
fib : int -> int = <fun>
#trace "fib";;
The function fib is now traced.
- : unit = ()
#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";;
The function fib is no longer traced.
- : unit = ()

How to debug my polymorphic function ?

The difficulty here, is that the output of the trace system is not very informative in case of polymorphic arguments and/or results. Consider a sorting algorithm (say bubble sort):

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";;
The function one_pass_vect is now traced.
- : unit = ()

#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 = ()

The function one_pass_vect being polymorphic, its vector argument is printed as a vector containing polymorphic values, [|<poly>; <poly>; <poly>|], and thus we cannot properly follow the computation.

How to debug my polymorphic function ?

A simple way to overcome this problem is to define a monomorphic version of the faulty function. This is fairly easy using a type constraint. Generally speaking, this allows a proper understanding of the error in the definition of the polymorphic function. Once this has been corrected, you just have to suppress the type constraint to revert to a polymorphic version of the function. For our sorting routine, a single type constraint on the argument of the exchange function warranties a monomorphic typing, that allows a proper trace of function calls:

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";;
The function one_pass_vect is now traced.
- : unit = ()
#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 = ()

How to trace a predefined function ?

Predefined functions are traceable as any other function, if we omit the problems related to polymorphism of the preceding section, but predefined functions exhibit a new phenomenon: the user shares these functions with the interactive system. This implies that every call to the function that the interactive system may do, are now traced as if they were yours.
This may be surprising:

#trace "map";;
The function map is now traced.
#[];;
map <-- <fun>
map --> <fun>
map* <-- [<poly>]
map* --> [<poly>]
- : 'a list = []

The map function is used by the interactive system during the compilation of the phrase (syntax analysis, type checking, compilation, linking and evaluation of the input Caml phrase). If you don't need this behavior, you must define your own version of the predefined function, using a dummy new definition, and trace this new function equivalent to the predefined one:

#untrace"map";;
map <-- <fun>
map --> <fun>
map* <-- [<poly>]
map* --> [<poly>]
The function map is no longer traced.
- : unit = ()
#let map f l = map f l;;
map : ('a -> 'b) -> 'a list -> 'b list = <fun>
#trace "map";;
The function map is now traced.
- : unit = ()
#[];;
- : 'a list = []
#map succ [1];;
map <-- <fun>
map --> <fun>
map* <-- [<poly>]
map* --> [<poly>]
- : int list = [2]

When traced my program is looping ?

This is a subtle particular case of the preceding phenomenon: if you trace one of the predefined functions used during the trace, the trace mechanism loops. This appears for instance if you try to trace one of the printing functions defined in the format pretty-printer, that the trace uses to output values. If you really need to trace these functions you must use the same trick as explained in the preceding paragraph: redefine the functions before any use in your program, then trace the new versions.

Is there a stepping facility ?

To keep track of assignments to data structures and mutable variables in a program, the trace facility is not powerful enough. You need an extra mechanism to stop the program in any place and ask for internal values: that is a symbolic debugger with its stepping feature.

Stepping a functional program has a meaning which is a bit weird to define and understand. Let me say that we use the notion of runtime events that happen for instance when a parameter is passed to a function or when entering a pattern matching, or selecting a clause in a pattern matching. Computation progress is taken into account by these events, independently of the instructions executed on the hardware.

Although this is difficult to implement, there exists such a debugger for Caml under Unix: this is the camldebug user contribution. Unfortunately its implementation uses advanced features of Unix, hence it is not directly portable under Macintosh or PC. On these machines, you must use the trace or explicit messages inserted here and there into the program.

In fact, for complex programs, it is likely the case that the programmer will use explicit printing to find the bugs, since this methodology allows the reduction of the trace material: only useful data are printed and special purpose formats are more suited to get the relevant information, than what can be output automatically by the generic pretty-printer used by the trace mechanism.


Caml home page Last modified: Friday, March 26, 2004
Copyright © 1995 - 2004, INRIA all rights reserved.

Contact the author Pierre.Weis@inria.fr