1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305
|
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/REC-html40/loose.dtd">
<html>
<head>
<title>Source Level Debugging in Poly/ML</title>
</head>
<body bgcolor="#FFFFFF">
<h2>Source Level Debugging in Poly/ML</h2>
<p>Poly/ML includes a source-level debugger. You can use it to set breakpoints
in the program and print the values of local variables. To turn on debugging
for a particular piece of code set the compiler variable <tt>PolyML.Compiler.debug</tt>
to true. You can freely mix functions compiled with debugging on with
functions compiled with debugging off, you simply can't set a breakpoint in
a non-debuggable function or print its internal state. The debugging
control functions are all in the <tt>PolyML.Debug</tt> structure<tt>. </tt>It
is often convenient to open this structure before debugging a program.</p>
<h3><big>An Example Session.</big></h3>
<p>To see how debugging works we'll follow through an example session. We have a
small function that is supposed to add together a list of pairs of integers but it has an
error in it which causes it not to terminate. We turn on debugging and compile in
the program.</p>
<p><tt>> <strong>PolyML.Compiler.debug := true;</strong><br>
val it = () : unit<br>
> <strong>fun addList a l =<br>
let<br>
fun f (b,c) = a+b+c<br>
in<br>
case l of<br>
[] => a<br>
| (x::y) =><br>
let<br>
val v = f x<br>
val l' = y<br>
in<br>
addList v l<br>
end<br>
end;</strong><br>
<br>
val addList = fn : int -> (int * int) list -> int<br>
> <strong>open PolyML.Debug;</strong></tt></p>
<p>We set a breakpoint in the function<tt> f</tt> using the <tt>breakIn</tt> function and
apply the function to some arguments.</p>
<p><tt>> <strong>breakIn "f";</strong><br>
val it = () : unit<br>
> <strong>addList 0 [(1,2), (3, 4)];</strong></tt></p>
<p>The function prints the line number and stops at the breakpoint.</p>
<p><tt>line:3 function:addList()f<br>
debug> </tt></p>
<p>The name <tt>addList()f</tt> is the full name of the function and we could have used
this in place of <tt>f</tt> when setting the breakpoint. The <tt>"debug>"</tt>
prompt is almost identical to the normal Poly/ML <tt>">"</tt> prompt.
The only difference is that variables which are in scope at the breakpoint are available
as though they were declared globally. So we can print the values of <tt>a</tt>, <tt>b</tt>,
<tt>c</tt> and <tt>l</tt>.</p>
<p><tt>debug> <strong>a;</strong><br>
val it = 0 : int<br>
debug> <strong>b;</strong><br>
val it = 1 : int<br>
debug> <strong>c;</strong><br>
val it = 2 : int<br>
debug> <strong>l;</strong><br>
val it = [(1, 2), (3, 4)] : (int * int) list<br>
debug> </tt></p>
<p>In addition anything in the original top level is also available, provided it is not
masked by a local declaration, so we can continue the program by calling the <tt>continue</tt>
function which we opened from <tt>PolyML.Debug</tt>.</p>
<p><tt>debug> <strong>continue();</strong><br>
val it = () : unit<br>
line:3 function:addList()f<br>
debug></tt></p>
<p>This continues and we find ourselves back in <tt>f</tt> at the breakpoint again.
We expect that and check the value of <tt>a</tt>.</p>
<p><tt>debug> <strong>a;</strong><br>
val it = 3 : int<br>
debug></tt></p>
<p>However when we check <tt>b</tt> something seems to be wrong and printing <tt>l</tt>
confirms it.</p>
<p><tt>debug> <strong>b;</strong><br>
val it = 1 : int<br>
debug> <strong>l;</strong><br>
val it = [(1, 2), (3, 4)] : (int * int) list<br>
debug></tt></p>
<p>We don't seem to be making any progress. We go up the stack to the recursive call
of <tt>addList</tt> in order to check that the value of <tt>l'</tt> is correct. We
have to go up twice because <tt>l'</tt> is not local to <tt>f</tt> nor is it available at
the point where <tt>f</tt> was called. It is only available at the point where <tt>addList</tt>
was called recursively.</p>
<p><tt>debug> <strong>up();</strong><br>
line:9 function:addList<br>
val it = () : unit<br>
debug> <strong>up();</strong><br>
line:12 function:addList<br>
val it = () : unit<br>
debug><strong> l';</strong><br>
val it = [(3, 4)] : (int * int) list<br>
debug> </tt></p>
<p>This looks fine so the problem was not that <tt>l</tt>' had the wrong value. We
print the values of everything using the <tt>dump</tt> function to see if that helps.</p>
<p><tt>debug> <strong>dump();</strong><br>
Function addList()f: c = 2 b = 1 l = [(1, 2), (3, 4)] a = 3 <br>
Function addList: x = (1, 2) y = [(3, 4)] f = fn l = [(1, 2), (3, 4)] a = 3 <br>
Function addList: l' = [(3, 4)] v = 3 x = (1, 2) y = [(3, 4)] f = fn<br>
l = [(1, 2), (3, 4)] a = 0 <br>
<br>
val it = () : unit</tt></p>
<p>At this stage it is clear that we are passing the original value of <tt>l</tt> rather
than what we intended,<tt> l'</tt>. We abort the function by typing control-C and f.</p>
<p><tt>debug> <strong>^C</strong><br>
=><strong>f</strong><br>
Compilation interrupted<br>
Pass exception to function being debugged (y/n)?<strong>y</strong><br>
Exception- Interrupt raised<br>
> </tt></p>
<p>This returns us to the top level. We now fix the error and clear the breakpoint. </p>
<p><tt>> <strong>fun addList a l =<br>
let<br>
fun f (b,c) = a+b+c<br>
in<br>
case l of<br>
[] => a<br>
| (x::y) =><br>
let<br>
val v = f x<br>
val l' = y<br>
in<br>
addList v l'<br>
end<br>
end;</strong><br>
val addList = fn : int -> (int * int) list -> int<br>
> <strong>clearIn "f";</strong><br>
val it = () : unit</tt></p>
<p>As a final check we turn on tracing to check that the values are as we expect and run
the program with the same input as before.</p>
<p><tt>> <strong>trace true;</strong><br>
val it = () : unit<br>
> <strong>addList 0 [(1,2), (3,4)];</strong><br>
addList entered l = [(1, 2), (3, 4)] a = 0 <br>
addList()f entered c = 2 b = 1 l = [(1, 2), (3, 4)] a = 0 <br>
addList()f returned 3<br>
addList entered l = [(3, 4)] a = 3 <br>
addList()f entered c = 4 b = 3 l = [(3, 4)] a = 3 <br>
addList()f returned 10<br>
addList entered l = [] a = 10 <br>
addList returned 10<br>
addList returned 10<br>
addList returned 10<br>
val it = 10 : int<br>
> </tt></p>
<p>This seems to have worked fine so we can now turn off <tt>PolyML.Compiler.debug</tt>
and compile the function without debugging.</p>
<p> </p>
<h3><big>Reference</big></h3>
<h3>Tracing program execution</h3>
<p><tt> val trace = fn : bool -> unit<br>
</tt>The <tt>trace</tt> function turns on and off function tracing. Function
tracing prints the arguments and results of every debuggable function.</p>
<h3>Breakpoints</h3>
<p><tt> val breakAt = fn : string * int -> unit<br>
val breakIn = fn : string -> unit<br>
val breakEx = fn : exn -> unit<br>
val clearAt = fn : string * int -> unit<br>
val clearIn = fn : string -> unit<br/>
val clearEx = fn : exn -> unit</tt></p>
<p>Breakpoints can be set with the <tt>breakAt</tt>, <tt>breakIn</tt> or <tt>breakEx</tt>
functions and cleared with <tt>clearAt</tt>, clearIn or <tt>clearEx</tt>.
<tt>breakAt</tt> takes a file name and line number and <tt>breakIn</tt> a function
name. The file name and line have to given exactly otherwise the breakpoint
will not work. <tt>breakIn</tt> is more flexible about the function name.
It can be the function name used in the declaration (e.g. <tt>f</tt>) or the
full "path name". The latter is useful when the program contains
several functions with the same name since setting a breakpoint in <tt>f</tt>
stops in any function called <tt>f</tt>. <tt>breakIn</tt> and <tt>breakAt</tt>
simply record that you want a breakpoint. There is no check when they
are called that the appropriate location actually exists and you can set a breakpoint
for a function and then compile it later. <tt>breakEx</tt> sets a breakpoint
for a particular exception and causes the program to stop at the end of the
function that raises the exception unless it is also handled there. The argument
is the exception to trap. It is possible to trap exceptions that take a parameter.
Just provide it with a dummy parameter to create a value of type <tt>exn</tt>
that can be passed to <tt>breakEx</tt>. The actual parameter value will be ignored
and the debugger will be entered whenever the exception is raised with any parameter
value. </p>
<p>When a breakpoint is reached the program stops with a <tt>debug></tt> prompt.
This is a normal Poly/ML top-level and you can type any ML expression. The only
difference is that local variables in the function being debugged are available as though
they had been declared at the top-level. You can print them or use them in any way
that you could with a normal variable. This includes local functions which can be
applied to local values, constants or globals. You cannot change the value of a variable
unless it is a reference. At a breakpoint you can continue or single-step the
program or you can raise an exception. This is usually the most convenient way of
aborting a program and getting back to the normal top-level unless the program has a
handler for the exception you raise.</p>
<h3>Single Stepping and Continuing</h3>
<p><tt>val continue = fn : unit -> unit<br>
val step = fn : unit -> unit<br>
val stepOver = fn : unit -> unit<br>
val stepOut = fn : unit -> unit</tt></p>
<p><tt>continue</tt> runs the program to the next breakpoint.<tt> step</tt>, <tt>stepOver</tt>
and <tt>stepOut</tt> are different ways of single-stepping through a function.
<tt>step</tt> runs the program up to the next breakable point which is often
the next source line. If evaluation involves calling a function then it may stop at
the beginning of the function. By contrast <tt>stepOver</tt> stops at the next line
within the current function only. <tt>stepOut</tt> goes further and stops only
within the function which called the current function. If a called function includes
a breakpoint they always stop there.</p>
<h3>Examining and Traversing the Stack</h3>
<p><tt>val up = fn : unit -> unit<br>
val down = fn : unit -> unit<br>
val dump = fn : unit -> unit<br>
val variables = fn : unit -> unit</tt></p>
<p><tt>up</tt> and <tt>down</tt> move the focus between called and calling functions
allowing you to view local variables at different levels. This is particularly
useful for recursive functions. <tt>The variables</tt> function prints all the
variables in scope at the current point. <tt>dump</tt> gives a complete stack trace.</p>
<h3>Notes</h3>
<p>The current implementation includes most values but not types or structures.
A recursive function is not available within the function itself.</p>
<p>The compiler adds debugging information which considerably increases the run time of
the program. It is probably best to turn debugging on only when it is needed.</p>
<p>The example shows debugging when all the variables have monomorphic types. The
debugger does not have access to any run-time type information so it cannot always know
how to print a value inside a polymorphic type. For example</p>
<p><tt>> fun map f [] = [] | map f (a::b) = f a :: map f b;<br>
val map = fn : ('a -> 'b) -> 'a list -> 'b list<br>
> breakIn "map";<br>
val it = () : unit<br>
> map (fn i => i+1) [1,2,3];<br>
line:1 function:map<br>
debug> a;<br>
val it = ? : 'a</tt></p>
<p>If you know the type is int you can add a type constraint.</p>
<p><tt>debug> a:int;<br>
val it = 1 : int<br>
debug> </tt></p>
<p>It is though equally possible to use the wrong constraint and crash Poly/ML.
Future versions of Poly/ML may treat polymorphic variables as opaque
which would prevent this but also prevent "safe" coercions.</p>
<p> </p>
</body>
</html>
|