<!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>The development version 4.1 of Poly/ML includes an experimental 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. It is possible to turn on and off tracing while a function is running by <a href="Intro1.html#ControlC">switching to the second shell</a>, typing the debugging commands and switching back again.</p> <h3>Breakpoints</h3> <p><tt> val breakAt = fn : string * int -> unit<br> val breakIn = fn : string -> unit<br> val clearAt = fn : string * int -> unit<br> val clearIn = fn : string -> unit</tt></p> <p>Breakpoints can be set with the <tt>breakAt</tt> or <tt>breakIn</tt> functions and cleared with <tt>clearAt</tt> or <tt>clearIn</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.</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>