Improving thisContext in the Debugger using First Class Variables
Have you ever tried to inspect a thisContext variable in the debugger?
Just interrupt Pharo by “CMD-.”, the debugger appears with the main UI loop waiting on a Semaphore ““[delaySemaphore wait] in Delay>>wait”.
So if I now write “thisContext” in that block (without saving, I just want to explore), I naively expect this to be the context we are in right now. The debugger even shows that below in the inspector (as “implicit thisContext”).
But if I inspect it, I get something completely different (try it, here is a picture):
So what happened?!
When you execute code in the debugger (via e.g. doIt, inspectIt or printIt), we give the code to the compiler to compile a DoIt method and then execute this DoIt method.
With thisContext being a very special PseudoVariable that always just leads to emitting the pushThisContext byte-code, hard-coded (thisContext, self and super are are for this reason on the “Syntax” Postcard for ST80).
You can see that by inspecting a method of a DoIt that accesses “thisContext”, just inspect “thisContext method” and look at the byte-code:
33 <52> pushThisContext
34 <80> send: method
35 <5C> returnTop
And as we execute the DoIt method, the pushThisContext byte-code pushes the context of the method we are executing, which is the DoIt method, not the method you are looking at in the debugger.
Can we do better: First Class Variables to the rescue
In Pharo, all Variables are modelled via a subclass of Variable. This includes the Pseudo Variables, the Variable subclass for ‘thisContext’ is ThisContextVariable. Variable names are never hard-coded, instead name-analysis looks up the name in a scope (the block or method that the variable is accessed in) and lookup goes to the outerScope until it reaches the global scope.
There is of course a reflective API, too. You can send #lookupVar: to any scope or the context, and the system will return the Variable of that name. For example
thisContext lookupVar: #self.
SmalltalkImage lookupVar: #CompilerClass.
Smalltalk globals lookupVar: #Object.
These are meta-objects describing Variables. As such, the API contains methods to allow the variable to set or return it’s value. Depending on the Variable, they need some object the read themselves from. Globals of course can just answer to #read, Slots have #read: to read from an Object. But all can read from a context using #readInContext:.
(Smalltalk globals lookupVar: #Object) readInContext: thisContext.
Of course, the readInContext: method just uses the other reflective read method after getting the value from the context if needed, e.g. Slots:
readInContext: aContext
^self read: aContext receiver
or ThisContextVariable:
readInContext: aContext
^aContext
This idea to have meta-object for Variables that provide a reflective API is very powerful, we use it in all tools: The inspector reads the values of the instance variables (and thus does not need to send a message like #instVarAt:, nice when you inspect proxies), the Debugger uses this to read temps, for example in DoIts. To read temps in the debugger that the programmer writes in the code, we have to support even the reading of temps that are actually not accessible in a block.
For this, when we compile code to be executed as a DoIt against a Context (like in the debugger), we use a special scope to lookup variables, the OCContextualDoItSemanticScope. It has a special version of lookupVar:
OCContextualDoItSemanticScope>>#lookupVar: name
(targetContext lookupVar: name) ifNotNil: [ :v | ^self importVariable: v].
^super lookupVar: name
importVariable: aVariable
^importedVariables
at: aVariable name
ifAbsentPut: [ aVariable asDoItVariableFrom: targetContext ]
Thus, it will wrap all variables that you look up in DoItVariable using asDoItVariableFrom:, which for now is implemented for temps (the others just return self):
asDoItVariableFrom: aContext
^ DoItVariable fromContext: aContext variable: self
The DoItVariable is a decorator: it decorates the original temp with a context, and changes the method that generate code to force the reflective read even at compile time:
emitValue: aMethodBuilder
aMethodBuilder
pushLiteral: self;
send: #read
with #read just forwarding the the original Variable:
read
^actualVariable readInContext: doItContext
Let’s fix it
So… what if we would add #asDoItVariableFrom: to ThisContextVariable? It would then, when we compile the DoIt, call #lookupVar: for thisContext, which would create the decorator and return it. From that point on, the DoItVariable named thisContext shadows the real thisContext, the compiler will ask it to generate code. That generated code will be the #read which will call #readInContext: on ThisContextVariable, which correctly returns the context.
Let’s try, copy, paste, and: YES! It just works. Now thisContext behaves just as we expected.
Nice, and that was just one method added!
PR for Pharo12 is here (Code review needed!)
Posted on May 24, 2023 #Pharo #Variables