>> 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.

Here is a PR:

Fix-CleanBlocks-StInspectorSelfNode #542
        https://github.com/pharo-spec/NewTools/pull/542 
<https://github.com/pharo-spec/NewTools/pull/542>

> On 30 May 2023, at 17:29, Marcus Denker <[email protected]> wrote:
> 
> 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
>> 
>> 
>> 
> 

Reply via email to