I have recently been getting into using the debugger, and I must say I'm impressed. It is very nice that you get the full power of the interactive compiler in very much the right value-binding context to understand how things are inside your functions.
I do have a few comments and queries though. In case any of this is platform specific, I have mainly been using it on x86 Linux with a 2.4.21 kernel.
1) The example in the documentation doesn't quite work as stated. The problem seems to be a general one: the only break-points seem to be at the head of a function (i.e., with the parameter bindings done, but not any bindings in let-expressions). When you go up the stack, values bound between the head of the function and the point of execution are not in scope. I.e., if you have something like:
fun f x = let y = h x; in g y end;
and break in g, when you go up to f, you can't see y (you can rerun the binding, but that may not be the right thing to do if h has side-effects).
2) If you mistype a function name in the parameter to breakIn, it doesn't cause an error (maybe this is a side-effect of a feature? it does seem to be very tolerant of the function names, e.g., you seem to be able leave off structure names and can stop in functions that aren't visible outside their structure).
3) It would be very nice indeed if you could break at points where an exception is raised. I can see that this might be a bit tricky because an awful lot of my ML code is failure-driven, so lots of exceptions get raised "unexceptionally" and you'd want a way of describing "exceptional" exceptions, In my case the ones I'm interested in are the ones that aren't going to be caught.
4) Is there are any run-time overhead in having code compiled with PolyML.Compiler.debug true?
5) ProofPower puts a wrapper around the compiler's read-eval-print loop (by calling PolyML.compiler with appropriate arguments to do the reading and printing). It would be nice if we could do the same with the debugger's read-eval-print loop.
Regards,
Rob.
Rob Arthan wrote:
- The example in the documentation doesn't quite work as stated. The problem
seems to be a general one: the only break-points seem to be at the head of a function (i.e., with the parameter bindings done, but not any bindings in let-expressions). When you go up the stack, values bound between the head of the function and the point of execution are not in scope. I.e., if you have something like:
fun f x = let y = h x; in g y end;
and break in g, when you go up to f, you can't see y (you can rerun the binding, but that may not be the right thing to do if h has side-effects).
It's even worse if the let-clause has a recursive call of f, e.g.
fun f 0 = 0 | f x = let val y = h (x, f (x - 1)); in g y end;
because you can't rerun the binding of h (x, f (x - 1)) to y since f is not bound in the debug environment provided at the head of itself.
I've had this problem on a few occasions and had to resort to making everything I need visible at the top-level and stepping through by manually following the execution and making the appropriate bindings.
I have since noticed that the manual, whilst describing the up and down operations, states `This is particularly useful for recursive functions.' I had to see the funny side...
Phil.
Rob, Thanks for your comments. The debugger was really an experiment to see what was possible to add a source-level debugger to Poly/ML and whether it would be useful. I didn't want to make changes to the low-level parts of the compiler so it works by adding code at the point where the machine-independent intermediate code is generated from the parse tree. That has a number of advantages, in particular code compiled with the debug switch on can be freely mixed with non-debuggable code.
- The example in the documentation doesn't quite work as stated. The problem
seems to be a general one: the only break-points seem to be at the head of a function (i.e., with the parameter bindings done, but not any bindings in let-expressions). When you go up the stack, values bound between the head of the function and the point of execution are not in scope. I.e., if you have something like:
fun f x = let y = h x; in g y end;
and break in g, when you go up to f, you can't see y (you can rerun the binding, but that may not be the right thing to do if h has side-effects).
It's clear that I've missed out an important point from the description: line breaks are significant. The debugger works by inserting extra code into the code-stream. Originally this just provided the ability to set a break point on the line but I later expanded it to record the environment. This code adds a very considerable overhead at run-time so it is currently only inserted at the start of a function or if the source line has changed. So, the simple way to get what you want is to split the function onto several lines: fun f x = let val y = h x in g y end;
Only inserting the code on a new line makes sense if all the code is doing is checking for a breakpoint. There's no point in having two breakpoints on the same line if the user can't specify which one to break at. From your comments it seems that this isn't the right thing to do for the environment and I guess this should be changed. My experience with source-level debuggers has been with debuggers for C and C++ where everything is line-based. For ML where functions are often small that probably isn't right.
- If you mistype a function name in the parameter to breakIn, it doesn't
cause an error (maybe this is a side-effect of a feature? it does seem to be very tolerant of the function names, e.g., you seem to be able leave off structure names and can stop in functions that aren't visible outside their structure).
Setting a break-point merely records the request in a table. When the code is actually executed the debugging code in the prelude of the function looks to see if there is an entry in the table and breaks if there is.
- It would be very nice indeed if you could break at points where an
exception is raised. I can see that this might be a bit tricky because an awful lot of my ML code is failure-driven, so lots of exceptions get raised "unexceptionally" and you'd want a way of describing "exceptional" exceptions, In my case the ones I'm interested in are the ones that aren't going to be caught.
You can use PolyML.exception_trace to produce a trace of exceptions that are not caught. Unlike the debugger this adds almost no overhead at run-time. You can then set explicit breakpoints where the exceptions are raised. I'll think about this but I think there may be problems with the interaction between the debugger and the low-level exception mechanism.
- Is there are any run-time overhead in having code compiled with
PolyML.Compiler.debug true?
Yes, a lot. Whenever a debugged function is entered there is a call to the debugger and a similar call at the exit. In addition there is a call whenever a line number has changed, provided there is ML code on it. These calls build up an environment structure which adds garbage-collection load. In addition many optimisations are in effect disabled by the need to keep the environment around, e.g. tail-recursion removal.
- ProofPower puts a wrapper around the compiler's read-eval-print loop (by
calling PolyML.compiler with appropriate arguments to do the reading and printing). It would be nice if we could do the same with the debugger's read-eval-print loop.
I'll think about that but I don't know whether it would be possible. The debugger needs to be able to create its read-eval-print loop dynamically so there would have to be a way to register your own function.
Regards, David.
David,
Thanks for the info.
... You can use PolyML.exception_trace to produce a trace of exceptions that are not caught. Unlike the debugger this adds almost no overhead at run-time. You can then set explicit breakpoints where the exceptions are raised. I'll think about this but I think there may be problems with the interaction between the debugger and the low-level exception mechanism. ...
Unfortunately PolyML.exception_trace almost never gives me useful results. I think this maybe because I'm nearly always executing code via PolyML.compiler. Does that sound right? And is there a work-around?
Regards,
Rob.
Rob, Rob Arthan wrote:
... You can use PolyML.exception_trace to produce a trace of exceptions that are not caught. Unlike the debugger this adds almost no overhead at run-time. You can then set explicit breakpoints where the exceptions are raised. I'll think about this but I think there may be problems with the interaction between the debugger and the low-level exception mechanism. ...
Unfortunately PolyML.exception_trace almost never gives me useful results. I think this maybe because I'm nearly always executing code via PolyML.compiler. Does that sound right? And is there a work-around?
The most likely reason is that the function that is raising the exception has been inserted inline into a calling function. This can happen through several levels so it may not be obvious which source function raised the exception. You can try reducing PolyML.Compiler.maxInlineSize and recompiling.
Another possibility, although unlikely, is that you have complex pattern matching on exceptions combining selection on exceptions and on their parameters. In that case the compiler may have had to insert a "catch-all" handler, discriminate the pattern using the general pattern matching code and reraise any unmatched exceptions. The exception trace will then show the exception as raised at the handler.
Regards, David.
David,
On Wednesday 21 Jul 2004 3:47 pm, David Matthews wrote:
Rob,
Rob Arthan wrote:
... Unfortunately PolyML.exception_trace almost never gives me useful results. I think this maybe because I'm nearly always executing code via PolyML.compiler. Does that sound right? And is there a work-around?
The most likely reason is that the function that is raising the exception has been inserted inline into a calling function. This can happen through several levels so it may not be obvious which source function raised the exception. You can try reducing PolyML.Compiler.maxInlineSize and recompiling. ...
How about the following?
(**** ML BEGINS ****) PolyML.Compiler.maxInlineSize := 0;
fun g1 () : int = ( raise Div );
fun g2 () = ( g1 () );
fun g3 () = ( g2() );
fun g4() = ( g3() );
PolyML.exception_trace g4; (**** ML ENDS ****)
Even if I run this on the ML compiler database (i.e., without any of my ProofPower code in the way), I get an empty stack trace.
Regards,
Rob.
Rob,
Rob Arthan wrote:
David,
On Wednesday 21 Jul 2004 3:47 pm, David Matthews wrote:
Rob,
Rob Arthan wrote:
... Unfortunately PolyML.exception_trace almost never gives me useful results. I think this maybe because I'm nearly always executing code via PolyML.compiler. Does that sound right? And is there a work-around?
The most likely reason is that the function that is raising the exception has been inserted inline into a calling function. This can happen through several levels so it may not be obvious which source function raised the exception. You can try reducing PolyML.Compiler.maxInlineSize and recompiling. ...
How about the following?
(**** ML BEGINS ****) PolyML.Compiler.maxInlineSize := 0;
fun g1 () : int = ( raise Div );
fun g2 () = ( g1 () );
fun g3 () = ( g2() );
fun g4() = ( g3() );
PolyML.exception_trace g4; (**** ML ENDS ****)
Even if I run this on the ML compiler database (i.e., without any of my ProofPower code in the way), I get an empty stack trace.
Yes, thanks for pointing this out. That's another reason for missing entries in the stack trace: tail recursion removal. The exception trace is simply the return entries on the stack. In this case the functions are tail-recursive so the function calls are implemented as jumps, leaving nothing on the return stack. If you replace the functions by multiple calls, e.g. fun g2 () = ( g1 (); g1() ); then you will get the trace:
Exception trace for exception - Div g2(1) g3(1) g4(1) End of trace
Whether all this makes the exception trace useless or not depends a lot on your coding style.
The idea of PolyML.exception_trace is that it can provide a trace without any impact on the run-time performance of the program, apart from a very small overhead to set up the trace. In contrast, turning on the debugging option in the compiler can add a very significant overhead.
Regards, David.