In this post I will quickly go over a nice new demo-feature we implemented in Pinocchio: Parallel Debugging.
The idea with Parallel Debugging is that you might have a small piece of code that fails (most often a test), but you have no idea why: another piece of code, which looks exactly the same to you is succeeding! How can this ever happen?
This situation actually occurred when my super-student Cami was not really awake yet and he wrote a test for the IdentitySetBucket class. This is, as the name specifies, an implementation of the Bucket class that’s going to be used for IdentitySets. IdentitySets are like in standard Smalltalk, Sets that compare elements with #== (pointer equality).
Now Cami wrote the following test:
testIncludesMixed
(b includes: #key) should = false.
b add: #key.
(b includes: #key) should = true.
(b includes: 'key') should = false.
(b includes: #key2) should = false.
b add: 'key2'.
(b includes: #key) should = true.
(b includes: 'key') should = false.
(b includes: #key2) should = false.
b add: #key2.
(b includes: #key2) should be: true.
(b includes: 'key2') should be: true.
(try to not mind the not so clean usage of Pexample, our own fork of Phexample for Pinocchio)
In this piece of code, according to Cami, but last 2 lines should do exactly the same; one should find a string, and the other find a symbol. However, he noticed that even while the symbol got retrieved; the string didn’t!
ParallelDebugger to the rescue!
Rather than just running the tests separately and being confused why they behave differently; lets use a debugger that actually runs them next to each other. It runs different pieces of code it gets in in parallel (as coroutines), switching between routines at each send and each return from send; constantly comparing if the input/output is the same for all routines.
PParallelDebugger interpret:
(Array
with: [ (b includes: 'key2') should be: true ]
with: [ (b includes: #key2) should be: true ])
This is what happens when you execute the piece of code (sorry; at the moment we only have stderr output; would be nicer to inspect with a GUI)
1: BlockClosure@BlockClosure>>#value (0)
2: BlockClosure@BlockClosure>>#value (0)
1: IdentitySetBucket@IdentitySetBucket>>#includes: (1)
2: IdentitySetBucket@IdentitySetBucket>>#includes: (1)
1: Continue class@Continue class>>#on: (1)
2: Continue class@Continue class>>#on: (1)
1: BlockClosure@BlockClosure>>#value: (1)
2: BlockClosure@BlockClosure>>#value: (1)
1: IdentitySetBucket@IdentitySetBucket>>#do: (1)
2: IdentitySetBucket@IdentitySetBucket>>#do: (1)
1: SmallInt@SmallInt>>#to:do: (2)
2: SmallInt@SmallInt>>#to:do: (2)
1: BlockClosure@BlockClosure>>#whileTrue: (1)
2: BlockClosure@BlockClosure>>#whileTrue: (1)
1: BlockClosure@BlockClosure>>#value (0)
2: BlockClosure@BlockClosure>>#value (0)
1: SmallInt@SmallInt>>#<= (1)
2: SmallInt@SmallInt>>#<= (1)
1: SmallInt@SmallInt>>#> (1)
2: SmallInt@SmallInt>>#> (1)
1 --> False
2 --> False
1: False@False>>#not (0)
2: False@False>>#not (0)
1 --> True
2 --> True
1 --> True
2 --> True
1 --> True
2 --> True
1: True@True>>#ifTrue: (1)
2: True@True>>#ifTrue: (1)
1: BlockClosure@BlockClosure>>#value (0)
2: BlockClosure@BlockClosure>>#value (0)
1: BlockClosure@BlockClosure>>#value (0)
2: BlockClosure@BlockClosure>>#value (0)
1: BlockClosure@BlockClosure>>#value: (1)
2: BlockClosure@BlockClosure>>#value: (1)
1: IdentitySetBucket@IdentitySetBucket>>#at: (1)
2: IdentitySetBucket@IdentitySetBucket>>#at: (1)
1 --> #'key'
2 --> #'key'
1: BlockClosure@BlockClosure>>#value: (1)
2: BlockClosure@BlockClosure>>#value: (1)
1: Symbol@Symbol>>#== (1)
2: Symbol@Symbol>>#== (1)
1 --> False
2 --> False
1: False@False>>#ifTrue: (1)
2: False@False>>#ifTrue: (1)
1 --> Nil
2 --> Nil
1 --> Nil
2 --> Nil
1 --> Nil
2 --> Nil
1: SmallInt@SmallInt>>#+ (1)
2: SmallInt@SmallInt>>#+ (1)
1 --> SmallInt
2 --> SmallInt
1 --> SmallInt
2 --> SmallInt
1: BlockClosure@BlockClosure>>#whileTrue: (1)
2: BlockClosure@BlockClosure>>#whileTrue: (1)
1: BlockClosure@BlockClosure>>#value (0)
2: BlockClosure@BlockClosure>>#value (0)
1: SmallInt@SmallInt>>#<= (1)
2: SmallInt@SmallInt>>#<= (1)
1: SmallInt@SmallInt>>#> (1)
2: SmallInt@SmallInt>>#> (1)
1 --> False
2 --> False
1: False@False>>#not (0)
2: False@False>>#not (0)
1 --> True
2 --> True
1 --> True
2 --> True
1 --> True
2 --> True
1: True@True>>#ifTrue: (1)
2: True@True>>#ifTrue: (1)
1: BlockClosure@BlockClosure>>#value (0)
2: BlockClosure@BlockClosure>>#value (0)
1: BlockClosure@BlockClosure>>#value (0)
2: BlockClosure@BlockClosure>>#value (0)
1: BlockClosure@BlockClosure>>#value: (1)
2: BlockClosure@BlockClosure>>#value: (1)
1: IdentitySetBucket@IdentitySetBucket>>#at: (1)
2: IdentitySetBucket@IdentitySetBucket>>#at: (1)
1 --> 'key2'
2 --> 'key2'
1: BlockClosure@BlockClosure>>#value: (1)
2: BlockClosure@BlockClosure>>#value: (1)
1: String@String>>#== (1)
2: String@String>>#== (1)
1 --> False
2 --> False
1: False@False>>#ifTrue: (1)
2: False@False>>#ifTrue: (1)
1 --> Nil
2 --> Nil
1 --> Nil
2 --> Nil
1 --> Nil
2 --> Nil
1: SmallInt@SmallInt>>#+ (1)
2: SmallInt@SmallInt>>#+ (1)
1 --> SmallInt
2 --> SmallInt
1 --> SmallInt
2 --> SmallInt
1: BlockClosure@BlockClosure>>#whileTrue: (1)
2: BlockClosure@BlockClosure>>#whileTrue: (1)
1: BlockClosure@BlockClosure>>#value (0)
2: BlockClosure@BlockClosure>>#value (0)
1: SmallInt@SmallInt>>#<= (1)
2: SmallInt@SmallInt>>#<= (1)
1: SmallInt@SmallInt>>#> (1)
2: SmallInt@SmallInt>>#> (1)
1 --> False
2 --> False
1: False@False>>#not (0)
2: False@False>>#not (0)
1 --> True
2 --> True
1 --> True
2 --> True
1 --> True
2 --> True
1: True@True>>#ifTrue: (1)
2: True@True>>#ifTrue: (1)
1: BlockClosure@BlockClosure>>#value (0)
2: BlockClosure@BlockClosure>>#value (0)
1: BlockClosure@BlockClosure>>#value (0)
2: BlockClosure@BlockClosure>>#value (0)
1: BlockClosure@BlockClosure>>#value: (1)
2: BlockClosure@BlockClosure>>#value: (1)
1: IdentitySetBucket@IdentitySetBucket>>#at: (1)
2: IdentitySetBucket@IdentitySetBucket>>#at: (1)
1 --> #'key2'
2 --> #'key2'
1: BlockClosure@BlockClosure>>#value: (1)
2: BlockClosure@BlockClosure>>#value: (1)
1: Symbol@Symbol>>#== (1)
2: Symbol@Symbol>>#== (1)
1 --> False
2 --> True
Expected: false but got: true
In this snippet, two parts of the trace are of importance for the bug at hand:
1: IdentitySetBucket@IdentitySetBucket>>#at: (1)
2: IdentitySetBucket@IdentitySetBucket>>#at: (1)
1 --> 'key2'
2 --> 'key2'
1: BlockClosure@BlockClosure>>#value: (1)
2: BlockClosure@BlockClosure>>#value: (1)
1: String@String>>#== (1)
2: String@String>>#== (1)
1 --> False
2 --> False
and
1: IdentitySetBucket@IdentitySetBucket>>#at: (1)
2: IdentitySetBucket@IdentitySetBucket>>#at: (1)
1 --> #'key2'
2 --> #'key2'
1: BlockClosure@BlockClosure>>#value: (1)
2: BlockClosure@BlockClosure>>#value: (1)
1: Symbol@Symbol>>#== (1)
2: Symbol@Symbol>>#== (1)
1 --> False
2 --> True
The first one basically says that it tried to compare the input string and symbol with the string
. This should actually already have resulted in a difference in the trace; but it didn’t. Then later on in the second snippet, it actually diverges and
is matched with the symbol but not the string. So this failure is to be expected. Now you can see that the method
on String doesn’t behave as expected while it does on Symbol. This is obvious as String that are
are not necessarily
. This is the whole point of the Identity vs normal Set, Dictionary, …
This is pretty cool and explains us nicely why the code failed.
Now the interesting part is how this whole debugger is implemented in the first place. It is basically a subclass of a SteppingInterpreter, a subclass of Interpreter that evaluates a
before each evaluation of a message send. By subclassing the SteppingInterpreter, implementing the ParallelDebugger comes down to two methods (methods for indentation are left out for readability; the whole source code is available at SqueakSource)
interpret: closures
stepBlock := [ :receiver :class :message :action |
results put: (receiver@class@message).
self show: continuations position asString, ': ', receiver class name, '@', class name, '>>', message.
self interpretNext.
results put: action value.
self show: continuations position asString, ' --> ', results current inspectName.
self interpretNext.
results current
].
results := StatefulArray new: closures size.
continuations := StatefulArray new: closures size.
closures do: [ :aClosure |
Continuation on: [ :startNext |
continuations nextPut: (startNext@nil).
^ super interpret: aClosure.
]
]
This method is the main
of the debugger. It overwrites the default
method, that takes an object (generally a BlockClosure) as input and evaluates the sending of
to it:
interpret: aClosure
^ self send: (Message new selector: #value) to: aClosure
It sets the
to a block that stores the information of the send in
and prints it, before switching to the next routine. Then when the control is given back to this routine, it actually executes the send (the action that’s passed in is a block created by the SteppingInterpreter to make it easy for subclasses to actually do the send; so sending
to the action will actually perform the send). The result of the send is then stored in the results before switching to the next routine. When the interpreter finally decides to come back to this routine, it returns the result to the place that send the message in the first place.
The whole loop is started by looping over the input closures. The next iteration of the booting loop is stored as the continuation of the next coroutine. Then we start the current interpretation by letting the SteppingInterpreter interpret the current closure.
Now when the Interpreter hits the first send in the code, it will jump out to the step block that will continue the next routine. The next routine in the beginning will be the continuation in the
loop at the bottom, so it will also jumpstart the second closure; and so on, until all closures have been started. (
on StatefulArray has been implemented so that it ignores the value when at the end of the array. This avoids storing a continuation for starting a fictional closure beyond the end of the closures array)
Once the first coroutine completely finishes, all coroutines should be finished, so rather than continuing the
after the
super interpret: aClosure
, we return to the code that started the debugger in the first place.
Now lets look at the piece of code that handles the switching of interpreters and checking of the intermediate results:
interpretNext
| cont |
Continuation on: [ :aContinuation |
continuations put: (aContinuation@context).
continuations ifAtEnd: [
|test|
test := results first.
results doRest: [ :result | result should be: test ].
cont := continuations first.
context := cont y.
cont x continue.
].
results next.
cont := continuations next.
context := cont y.
cont x continue.
]
This piece of code captures a continuation every time it gets invoked. It stores this continuation and the context of the interpreter (the environment) in the continuations list. When not at the end of the list, it makes the results list point to the next element; it takes the next continuation and restores it. It is restored by restoring the context, and then continuing the continuation.
When we are at the end of the list of coroutines however, we take the first result and compare it to all the results produces by all the coroutines. This result will contain information about a message send when switching before doing the actual send; and later the return value of the send when switching after the send has returned.
When all values have been considered equal, we restart at the first coroutine.
And that’s all folks!
Continue reading →