2004.04.20

 

The Debugger is dead, long live the Debugger!

by Karel Thönissen

I wrote an earlier article about my dislike for debugger software. Debuggers are a symptom of poorly factored code and of a willingness to reinvent the wheel. If a computer program should be in a certain state at a certain position in the code, then this should be indicated with a contract. It is a waste of time, talent, energy and coffee to check that condition manually with a debugger every time the execution reaches that position in the code. Moreover, if the check is correct, then it should be an invariable part of the program, and not be stored in volatile human memory. If expressed with a contract, the test can be run over and over again, even when the programmer has long gone. Since the contract is visible in the code, it is also taken into account during reviews.

At Garabit we have developed a framework that allows us to specify contracts and tests, that makes it very hard for errors to slip through. The framework works brilliantly, and catches nearly all errors in our code. The hard part is getting the contracts right, but design always has been the hard part, and Design by Contract (DbC) only forces you to be explicit. It makes design as hard as it should have been in the first place. When compared to cowboy programming, DbC makes it harder to design and implement a routine, but the benefits are for the client of the routine and in better design and increased quality.

Our framework caught the errors, but certain types of contract violations were hard to trace. The error message clearly indicates which contract was violated by means of a file name and position. For checks and postconditions, this is enough information, because the offending code is in the vicinity of the violated contract. Unfortunately, that is not the case for precondition violations. The trouble we had with precondition violations was how to determine which client invoked the routine.

Ideally, the development environment would allow to reverse the direction of execution from the point of the precondition violation. Unfortunately, our IDE does not support this practice. Experienced debuggers may shout: 'Inspect the call stack with a debugger!'. The problem was that once we ran into a violation, the execution context was lost, so using the debugger for an inspection of the call stack became impossible.

Yesterday, we have been able to solve this problem. Just before the precondition violation terminates the execution, a breakpoint is set in the code, so that the execution is halted. The developer can either resume execution, read the violation report,and witness the termination of his program, or he can inspect the call stack to find the routine that broke the precondition contract.

This may sound trivial to implement, but it is far from that. The naive solution would be to set a breakpoint at the position just before a precondition violation is raised. However, this ignores two facts. Firstly, we do not want this mechanism to be in place with deployed code. For deployed code we want normal violation formantics without involvement of a debugger/ breakpoints. Secondly, during unit testing we consciously trigger precondition violations to test whether the contracts behave as expected and whether the test battery has been designed properly. The last thing we want is a set of unit tests that is interrupted because of breakpoints that are set automatically. We have carefully designed our unit tests so that they can run blindly, i.e. without any developer interaction whatsoever. We call that one-step unit testing, and we want to keep it that way. Only this way it is possible for each developer to execute all unit tests all of the time. So determining whether or not to set a breakpoint when a precondition is about to be violated requires some intricate logic.

With the new addition to our framework in place, we have a mechanism that not only discovers errors in source code very early without having to trace or watch the code, it also allows us to determine the location of the error exactly to the statement level in a split second. It is almost to good to be true. Loosely speaking, we have refuted the Heisenberg-principle of software engineering, knowing both the timing/presence and the location of an error. Notice that this is all with a language without native support for DbC and without assembly language programming or platform dependent code.

At Garabit, the use of debuggers already was minimal. Our developers estimate that our framework reduced their time spent in debuggers with 75% compared to earlier programming jobs they did. I estimate that I spent less than 10 minutes in the debugger per month. However, I always had some trouble convincing others to stamp out their last use of the debugger completely. From now on this will be simple: conventional debugging is a completely waste of time, talent and energy (never mind the coffee), to such an extent that it is obvious to everybody who has seen used framework and method. The debugger is finally superfluous, except for a tiny part, the call stack inspector.

Le Debugger est mort, vive le Debugger!