I did a PR:
https://github.com/pharo-project/pharo/pull/13855 differences: - guard for nil in #readInContext - two tests: SelfVariableTest >> testReadInContext [ | var | var := self class lookupVar: #self. self assert: (var readInContext: thisContext) identicalTo: self. "read from a block context" self assert: [(var readInContext: thisContext)] value identicalTo: self ] SelfVariableTest >> testReadInContextClean [ <compilerOptions: #( +optionCleanBlockClosure)> | var block | "if context is one stack we can read" block := [ (thisContext lookupVar: #self) readInContext: thisContext ]. self assert: block value equals: self. "but no chance if not, then it is nil" var := self class lookupVar: #self. block := [ thisContext ]. self assert: (var readInContext: block value ) equals: nil ] > On 29 May 2023, at 10:48, Marcus Denker <[email protected]> wrote: > > In the last post, we looked at reading the PseudoVariable thisContext in the > Debugger. A summary of that might be: inspecting a variable compiled a DoIt, > that DoIt it executed parametrised with the context that you look at in the > debugger. As it is, in the end, just another method executing, the byte-code > pushThisContext pushes the context of the executing DoIT method, not the > context that you look at in the debugger. > > For "self", this is of course the same. Take Pharo11, add and insect > "thisContext method inspect. self" > > 41 <52> pushThisContext > 42 <80> send: method > 43 <81> send: inspect > 44 <D8> pop > 45 <58> returnSelf > > > Thus even "self" is not really concerned with the context you are looking at, > it is the "self" of the DoIt. The nice thing is that this is always (in the > debugger) the correct object: In the case of blocks, the only time you look > at a block in a debugger to read self is if you are in the home method of the > block (the method that contains the definition of the block). And as the > "self" of a block closure is the "self" of the home context, you get the > value you expect. > > > # Self and Clean Blocks > > It gets interesting if you put clean blocks into the picture. Clean Blocks > are blocks that just use information that is available at compile time. The > compiler thus can pre-allocate them. For this discussion, we need to know > that clean blocks do not have an outer Context, and self of the block is > always nil. > > Clean block support is there, you can enable it and even re-compile the whole > image, but there are still some details to be improved before we can enable > it. One of that is to fix all the debugger infrastructure to deal with blocks > that have no receiver and no home context, this post thus is one tiny step of > that work. > > For this post, we just need to compile one method with clean blocks enabled, > we create a class and method to play with (e.g. class TT with method tt): > > > ``` > tt > <compilerOptions: #(+ optionCleanBlockClosure)> > [ 1halt ] value > ``` > > (or instead of the pragma, enable the setting globally with the Settings > browser) > > With just sending a message to 1, the block can be compiled as a clean block. > The halt will mean that the debugger opens on the context of the clean block > being executed. > > Let's do that, execute "TT new tt" and we get a debugger: > > <1CleanBlockDebugger.jpeg> > > > It is interesting to look at the context, if you inspect the context, you see > that the receiver is nil. Looking at the block closure, we see that it is > indeed a CleanBlockClosure, outerContext is nil (and thus it can not follow > the outerContext till it finds the home context). > > So if we now just write "self" in the block and expect it (we do not save, we > just want to explore), it evaluates to "nil": > > > <2SelfInClean.jpeg> > This is of course correct: The debugger looks at the home context of the > block, we ask context for "self" and self in a context evaluating a clean > block is nil, as the block does not know self. > > But I am sure you are not happy with that. It feels wrong. The reason is that > if you would accept the method (recompile), restart, then self would be > "correct" (the instance of TT). This is because the compiler now sees the > self, realizes that this block requires a full block and compiles a full > block. > > This is similar to accessing temps in blocks that do not access them. Change > the method to have a temp, but one that is not accessed in the block: > > ``` > tt > > | temp | > temp := 1. > [ 1halt ] value. > ^temp. > ``` > > If you write "temp" into the block and inspect or print it, you get the value > of temp: > > > <3ReadTemp.jpeg> > The context of the executing block does not know the variable "temp" at all, > it's value is as unknown to it as the self is to the clean block. (This is > true both if compiled as a clean block or a full block). > > So why can we nevertheless read it? The reason: > > - Looking up the variable by name of course works (after all, the compiler > can compile a block that does access it, the compiler uses the same > data for variable lookup as the reflective subsystem does) > - The reflective read is carefully build to allow reading this variable even > from the block context that does not know it (by leveraging the stack of > contexts) > - We force the compiler to compile all temp variable accesses as reflective > accesses in a DoIt > > So if we can "fake" it for temps (both clean and non-clean blocks), can we do > it for "self in a clean block", too? > > If you think back at the discussion of reading thisContext, we fixed that by > forcing reflective read for thisContext, too. And of course the PR that > implements it, was done > already for all PseudoVariables. > > This means, reading self should, in Pharo12, already end up executing > SelfVariable>>#readInContext:, so let's just put a halt there, trigger a read > in > the debugger and, indeed: > > <4HaltInSelfRead.jpeg> > > > One thing we now see is that reflective read of self is implemented to do > exactly the same as "pushSelf", it returns the receiver of the context we > look at in the debugger: > > ``` > readInContext: aContext > self halt. > ^aContext receiver > ``` > > > But that is not really correct: in case of a block, it should be the receiver > of the *home* context. For all practical purposes (when in the debugger) they > are the same, and for Clean Blocks we even know that the home Context is > unknown... So how does this help if we would implement it like that: > > ``` > readInContext: aContext > ^aContext home receiver > ``` > > The trick is that we can find a home context for clean blocks in some > specific cases. Even though we can not get the home of a clean block via it's > outer context (as is is nil), we can find the home method if it happens to be > on the stack. And in the debugger, we are in exactly that case! > > So let's try to fix #home for clean blocks. > > ## The concept of active Home > > The Debugger already needs to know if the home is currently on the stack. For > that, it used a method #activeHome, in Pharo10 this looked like that: > > > activeHome > | methodReturnContext | > self isBlockContext ifFalse: [^self]. > self sender ifNil: [^nil]. > methodReturnContext := self methodReturnContext. > ^self sender findContextSuchThat: [:ctxt | ctxt = methodReturnContext] > > > #activeHome returns the #home if it is currently on the stack, the debugger > uses that to check if a “save and proceed” > is possible when editing code in a Block. Until Pharo11, it was implemented > to search up the sender chain until it finds the #home. > > But it can be rewritten to do the same, but checking for the #homeMethod and > using #findMethodContextSuchThat: > > activeHome > | homeMethod | > self isBlockContext ifFalse: [^self]. > homeMethod := self homeMethod. > ^self findMethodContextSuchThat: [:ctxt | ctxt method == homeMethod] > > > With #homeMethod being implemented to delegate to the bock: > > Context>>homeMethod > "Answer the method in which the receiver was defined, i.e. the context > from which an ^-return ] should return from. Note: implemented to not need > #home" > > ^ closureOrNil ifNil: [ self method ] ifNotNil: [ :closure | closure > homeMethod ] > > > Where, if no #home via the outerContext is available, it asks the > CompiledBlock: > > BlockClosure>>homeMethod > "return the home method. If no #home is available due to no > outerContext, use the compiledBlock" > ^ (self home > ifNotNil: [ :homeContext | homeContext ] > ifNil: [ self compiledBlock ]) method > > > Which uses the static #outerCode chain (CompiledBlocks encode a back-pointer > to the enclosing block or method), > with #method following #outerCode until it reaches a CompiledMethod: > > CompiledBlock>>method > "answer the compiled method that I am installed in, or nil if none.” > ^self outerCode method > > > This was already done in Pharo11, > > 11965-Implement-ContextactiveHome-without-using-home #12063 > https://github.com/pharo-project/pharo/pull/12063 > > > But we can now continue and use it to improve #home of Context, you see that > it already has a (bad) workaround when the outerContext is nil: > > > ``` > home > "Answer the context in which the receiver was defined, i.e. the context > from which an ^-return ] should return from." > > closureOrNil ifNil: [ ^ self ]. > "this happens for clean blocks. We should later check if it is not > better to return nil" > closureOrNil outerContext ifNil: [ ^ self ]. > ^ closureOrNil outerContext home > ``` > > > returning self when the outerContext is nil (when we are in a clean block) is > only correct for the home context of a clean block. > We can do better, and use #activeHome and search for it on the stack (with > the added bonus to correctly return nil if we do not find it). > > > ``` > home > "Answer the context in which the receiver was defined, i.e. the context > from which an ^-return ] should return from." > > closureOrNil ifNil: [ ^ self ]. > "no outerContext for clean blocks, we try to find it on the stack" > ^ closureOrNil outerContext > ifNil: [ self activeHome ] > ifNotNil: [:outer | outer home ] > ``` > > And it works! > > <5FixedReadSelf.jpeg> > > I am not sure if falling back to #activeHome in #home is really the best > (maybe explicitly handling the nil and do the fallback to activeHome > at the side of the caller is less magic), but we can change that easily later > if it turns out the be better. > > Another thing to look at is the inspector of the debugger showing self as nil > (first line, the implicit self), but his is for another time. > > Marcus > > >
