Previous Contents Next

Debugging Tools

There are two debugging tools. The first is a trace mechanism that can be used on the global functions in the toplevel loop. The second tool is a debugger that is not used in the normal toplevel loop. After a first program run it is possible to go back to breakpoints, and to inspect values or to restart certain functions with different arguments. This second tool only runs under Unix, because it duplicates the running process via a fork (see page ??).

Trace

The trace of a function is the list of the values of its parameters together with its result in the course of a program run.

The trace commands are directives in the toplevel loop. They allow to trace a function, stop its trace or to stop all active traces. These three directives are shown in the table below.
#trace name trace function name
#untrace name stop tracing function name
#untrace_all stop all traces

Here is a first example of the definition of a function f:

# let f x = x + 1;;
val f : int -> int = <fun>
# f 4;;
- : int = 5


Now we will trace this function, so that its arguments and its return value will be shown.

# #trace f;;
f is now traced.
# f 4;;
f <-- 4
f --> 5
- : int = 5
Passing of the argument 4 to f is shown, then the function f calculates the desired value and the result is returned and also shown. The arguments of a function call are indicated by a left arrow and the return value by an arrow to the right.

Functions of Several Arguments

Functions of several arguments (or functions returning a closure) are also traceable. Each argument passed is shown. To distinguish the different closures, the number of arguments already passed to the closures is marked with a *. Let the function verif_div take 4 numbers (a, b, q, r) corresponding to the integer division: 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


Its trace shows the passing of 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


Recursive Functions

The trace gives valuable information about recursive functions, e.g. poor stopping criteria are easily detected.

Let the function belongs_to which tests whether an integer belongs to a list of integers be defined in the following manner:

# let rec belongs_to (e : int) l = match l with
[] -> false
| t::q -> (e = t) || belongs_to e q ;;
val belongs_to : int -> int list -> bool = <fun>
# belongs_to 4 [3;5;7] ;;
- : bool = false
# belongs_to 4 [1; 2; 3; 4; 5; 6; 7; 8] ;;
- : bool = true


The trace of the function invocation belongs_to 4 [3;5;7] will show the four calls of this function and the results returned.

# #trace belongs_to ;;
belongs_to is now traced.
# belongs_to 4 [3;5;7] ;;
belongs_to <-- 4
belongs_to --> <fun>
belongs_to* <-- [3; 5; 7]
belongs_to <-- 4
belongs_to --> <fun>
belongs_to* <-- [5; 7]
belongs_to <-- 4
belongs_to --> <fun>
belongs_to* <-- [7]
belongs_to <-- 4
belongs_to --> <fun>
belongs_to* <-- []
belongs_to* --> false
belongs_to* --> false
belongs_to* --> false
belongs_to* --> false
- : bool = false


At each call of the function belongs_to the argument 4 and the list to search in are passed as arguments. When the list becomes empty, the functions return false as a return value which is passed along to each waiting recursive invocation.

The following example shows the section of the list when the element searched for appears:

# belongs_to 4 [1; 2; 3; 4; 5; 6; 7; 8] ;;
belongs_to <-- 4
belongs_to --> <fun>
belongs_to* <-- [1; 2; 3; 4; 5; 6; 7; 8]
belongs_to <-- 4
belongs_to --> <fun>
belongs_to* <-- [2; 3; 4; 5; 6; 7; 8]
belongs_to <-- 4
belongs_to --> <fun>
belongs_to* <-- [3; 4; 5; 6; 7; 8]
belongs_to <-- 4
belongs_to --> <fun>
belongs_to* <-- [4; 5; 6; 7; 8]
belongs_to* --> true
belongs_to* --> true
belongs_to* --> true
belongs_to* --> true
- : bool = true
As soon as 4 becomes head of the list, the functions return true which gets passed along to each waiting recursive invocation.

If the sequence of statements around || were changed, the function belongs_to would still return the right result but would always have to go over the complete list.

# let rec belongs_to (e : int) = function
[] -> false
| t::q -> belongs_to e q || (e = t) ;;
val belongs_to : int -> int list -> bool = <fun>
# #trace belongs_to ;;
belongs_to is now traced.
# belongs_to 3 [3;5;7] ;;
belongs_to <-- 3
belongs_to --> <fun>
belongs_to* <-- [3; 5; 7]
belongs_to <-- 3
belongs_to --> <fun>
belongs_to* <-- [5; 7]
belongs_to <-- 3
belongs_to --> <fun>
belongs_to* <-- [7]
belongs_to <-- 3
belongs_to --> <fun>
belongs_to* <-- []
belongs_to* --> false
belongs_to* --> false
belongs_to* --> false
belongs_to* --> true
- : bool = true
Even though 3 is the first element of the list, it is traversed completely. So, trace also provides a mechanism for the efficiency analysis of recursive functions.

Polymorphic Functions

The trace does not show the value corresponding to an argument of a parameterized type. If for example the function belongs_to can be written without an explicit type constraint:

# let rec belongs_to e l = match l with
[] -> false
| t::q -> (e = t) || belongs_to e q ;;
val belongs_to : 'a -> 'a list -> bool = <fun>
The type of the function belongs_to is now polymorphic, and the trace does no longer show the value of its arguments but replaces them with the indication (poly).

# #trace belongs_to ;;
belongs_to is now traced.
# belongs_to 3 [2;3;4] ;;
belongs_to <-- <poly>
belongs_to --> <fun>
belongs_to* <-- [<poly>; <poly>; <poly>]
belongs_to <-- <poly>
belongs_to --> <fun>
belongs_to* <-- [<poly>; <poly>]
belongs_to* --> true
belongs_to* --> true
- : bool = true


The Objective CAML toplevel loop can only show monomorphic types. Moreover, it only keeps the inferred types of global declarations. Therefore, after compilation of the expression belongs_to 3 [2;3;4], the toplevel loop in fact no longer possesses any further type information about the function belongs_to apart form the type 'a -> 'a list -> bool. The (monomorphic) types of 3 and [2;3;4] are lost, because the values do not keep any type information: this is static typing. This is the reason why the trace mechanism attributes the polymorphic types 'a and 'a list to the arguments of the function belongs_to and does not show their values.

It is this absence of typing information in values that entails the impossibility of constructing a generic print function of type 'a -> unit.

Local Functions

Local functions cannot be traced for the same reasons as above, relating again to static typing. Only global type declarations are kept in the environment of the toplevel loop. Still the following programming style is common:

# let belongs_to e l =
let rec bel_aux l = match l with
[] -> false
| t::q -> (e = t) || (bel_aux q)
in
bel_aux l;;
val belongs_to : 'a -> 'a list -> bool = <fun>
The global function only calls on the local function, which does the interesting part of the work.

Notes on Tracing

Tracing is actually the only multi-platform debugging tool. Its two weaknesses are the absence of tracing information for local functions and the inability to show the value of polymorphic parameters. This strongly restricts its usage, mainly during the first steps with the language.

Debug

ocamldebug, is a debugger in the usual sense of the word. It permits step-by-step execution, the insertion of breakpoints and the inspection and modification of values in the environment.

Single-stepping a program presupposes the knowledge of what comprises a program step. In imperative programming this is an easy enough notion: a step corresponds (more or less) to a single instruction of the language. But this definition does not make much sense in functional programming; one instead speaks of program events. These are applications, entries to functions, pattern matching, a conditional, a loop, an element of a sequence, etc.

Warning


This tool only runs under Unix.


Compiling with Debugging Mode

The -g compiler option produces a .cmo file that allows the generation of the necessary instructions for debugging. Only the bytecode compiler knows about this option. It is necessary to set this option during compilation of the files encompassing an application. Once the executable is produced, execution in debug mode can be accomplished with the following ocamldebug command:

ocamldebug [options] executable [arguments]

Take the following example file fact.ml which calculates the factorial function:

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;;


The main program in the file main.ml goes off on a long recursion after the call of Fact.fact on -1.

let x = ref 4;;
let go () =
x := -1;
Fact.fact !x;;
go();;


The two files are compiled with the -g option:
$ ocamlc -g -i -o fact.exe fact.ml main.ml
val fact : int -> int
val x : int ref
val go : unit -> int

Starting the Debugger

Once an executable is compiled with debug mode, it can be run in this mode.
$ ocamldebug fact.exe
        Objective Caml Debugger version 3.00

(ocd) 

Execution Control

Execution control is done via program events. It is possible to go forward and backwards by n program events, or to go forward or backwards to the next breakpoint (or the nth breakpoint). A breakpoint can be set on a function or a program event. The choice of language element is shown by line and column number or the number of characters. This locality may be relative to a module.

In the example below, a breakpoint is set at the fourth line of module Main:

(ocd) step 0
Loading program... done.
Time : 0
Beginning of program.
(ocd)  break @ Main 4
Breakpoint 1 at 5028 : file Main, line 4 column 3
The initialisations of the module are done before the actual program. This is the reason the breakpoint at line 4 occurs only after 5028 instructions.

We go forward or backwards in the execution either by program elements or by breakpoints. run and reverse run the program just to the next breakpoint. The first in the direction of program execution, the second in the backwards direction. The step command advanced by 1 or n program elements, entering into functions, next steps over them. backstep and previous respectively do the same in the backwards direction. finish finally completes the current functions invocations, whereas start returns to the program element before the function invocation.

To continue our example, we go forward to the breakpoint and then execute three program instructions:
(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

Inspection of Values

At a breakpoint, the values of variables in the activation record can be inspected. The print and display commands output the values associated with a variable according to the different depths.

We will print the value of n, then go back three steps to print the contents of 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}
Access to the fields of a record or via the index of an array is accepted by the printing commands.
(ocd) print x.contents
1 : int = -1

Execution Stack

The execution stack permits a visualization of the entanglement of function invocations. The backtrace or bt command shows the stack of calls. The up and down commands select the next or preceding activation record. Finally, the frame command gives a description of the current record.


Previous Contents Next