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
1
1
5
2
1
;;
- : bool = true
Its trace shows the passing of 4 arguments:
#
#trace
verif_div;;
verif_div is now traced.
# verif_div
1
1
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.