Last updated at 2:23 pm UTC on 16 January 2006
The method ContextPart>>quickStep, which is used by the debugger, just calls on ContextPart>>step, which in turn interprets the next bytecode. But before doing so, quickStep records the receiver in the class variable QuickStep, which is checked by the method ContextPart>>send:super:numArgs:.
If that method detects that it has been called indirectly from quickStep, it calls ContextPart>>quickSend:selector:to:with:super: instead of ContextPart>>send:selector:to:with:super: which it did before, and which it still does in other single-stepping situations.
ContextPart>>quickSend:selector:to:with:super: has the real trickery. We want to execute the send not by simulation, but by using the #perform: mechanism. However, the execution environment should match as much as possible the environment seen by the same code outside of the debugger.
We can't reach that goal completely, but we can become pretty close.
The reason that we cannot have exactly the same environment is that we must make sure that the executed code returns into the debugger instead of running freely. We're using an unwind-protected block to catch any nonlocal returns, and we manipulate context structures to make all the exceptions handlers visible to the executing code.
Here's the first part of the stack that you get when you execute the following:
and use the debugger's step button on the message #value.
- self halt. [thisContext stack] value
There are just two contexts in addition to those which you would see if this was not in the debugger.
-  in UndefinedObject>>DoIt
-  in MethodContext(ContextPart)>>quickSend:to:with:super:
-  in TextMorphEditor(ParagraphEditor)>>evaluateSelection
BlockContext>>ifCurtailed: is necessary to detect nonlocal returns. We'll return to that later.
 in MethodContext(ContextPart)>>quickSend:to:with:super: is the block executing our #perform:withArguments:inSuperclass: which is necessary to get the stuff really going.
As you see, the sender of the #ifCurtailed: message has been manipulated to point to the context in which the stepping takes place. By this trick, exception handlers which lie above the context being stepped will be executed when necessary.
If the #perform: returns normally, the sender manipulation will be undone, the returned value will be pushed in self (which is the current context of the simulated stack), and the context will be returned.
However, if we have a nonlocal return, things get a little messy. Nothing that half a dozen lines of Smalltalk can't fix, but it took me a whole evening to find just these 6 lines...
When a nonlocal return from a block happens and an unwind-protected block is in the context chain between the returning context and the sender of its home context, the returning (block) context gets the message #aboutToReturn:through: where the first argument is the value being returned, and the second argument is the unwind context.
Since the unwind block is called with zero arguments, we have to walk the sender chain to get at the context to return to (thisContext sender receiver) and the value being returned (thisContext sender tempAt: 1). The returning context is already in a half-returned state, that is, its pc points past the return bytecode. To show the return statement in the debugger, we need to skip back one byte. This also gives us the opportunity to look at the return bytecode to see whether it was a generic returnTop or one of the special return bytecodes which return self, true false or nil.
If it was a returnTop, we push the value being returned onto the stack of the returning context, so that the whole context looks just like it did before the return.
Now we change the sender of thisContext (which is the unwind block) to point to the sender of the #quickSend:... context, thereby evading the unwind machinery which would otherwise catch us if we tried a nonlocal return now from this unwind handler. The value returned from the block (contextToReturnTo) is actually returned to the sender of the method, and dutifully displayed by the debugger as the new current context.
Now, what do we do with exceptions being signalled in the quickStepped code?
One option would be to open another debugger. I think that's fairly inelegant, and wanted to show the exception-causing method in the debugger instead.
First I had to decide which exceptions to show. One possible option would be to just handle all possible exceptions. However, there are a number of exceptions whose default behavior is not to open a debugger but to return some special value. EndOfStream is an example. If we would catch all exceptions, every time a stepped method calls #next on a stream that has reached its end, the debugger would show this exception.
So I changed the Debugger class method #openContext:label:contents: to search the call stack for traces of quickStepping, and to reactivate the stepping debugger in this case. The actual method doing the search and manipulation is #informExistingDebugger:label:, and it does some more context manipulation stuff to throw away the two additional contexts created by the quickStep mechanism, and to return into the debugger. I won't go into this method in detail now, you can just read the code and try to see what it does.
One additional thought about exceptions: My current approach is to let installed exception handlers just handle the exceptions in debugged code. I don't know if that's a good idea, it would probably be better to stop the debugger when exceptions are signalled. However, I do not have enough experience to say whether one or the other is better.