[Vm-dev] Incomprehendable behaviour of ContextPart class
>>#contextOn:do:
Lars Wassermann
lars.wassermann at googlemail.com
Sun May 19 20:59:54 UTC 2013
On 05/19/2013 07:29 PM, Eliot Miranda wrote:
>
> On Sat, May 18, 2013 at 3:49 PM, Lars <lars.wassermann at googlemail.com
> <mailto:lars.wassermann at googlemail.com>> wrote:
>
>
> Hi Eliot,
> thank you for the analysis.
> The question is why the code works with the VM, but not if we step
> through it. What does the VM do that makes this behave correctly,
> although the temps-array is removed from the stack prior to
> returning from #contextOn:do:?
>
>
> the vm doesn't do anything special (* with one exception) because even
> though the image code pops off the temps array the return pushes it back
> almost immediately, before the temps array is used.
>
Well, the temps array is poped by #jump, then pushed back due to the
return, ... but then poped again because the returned to context is not
a top context. It's pc points to the pop bytecode (54?) which is there
because the return value of the #on:do: message send in #contextOn:do:
is not used. And at this point, the stepwise debugging breaks. But the
VM just skips over it and returns with the correct value. Why?
>
> * the exception is that when a context is created for a stack frame the
> code miust handle the fact that the stack may be empty and so the proxy
> context must have a nil where the missing stack entry is, not an
> uninitialized field. See the comment
> in updateStateOfSpouseContextForFrame:WithSP:.
>
> I ask because I need to recreate that behaviour in case it also
> appears in other places. The general project is creating a squeak
> research VM. That is also why I try to refrain from changing the
> image, retaining compatibility with Pharo/Squeak trunk.
>
> Thank you, Lars
>
> 2013/05/17 8:08 pm Eliot Miranda <eliot.miranda at gmail.com>
> <mailto:eliot.miranda at gmail.com>:
>>
>>
>>
>> Hi Lars,
>>
>> On Fri, May 17, 2013 at 9:36 AM, Lars Wassermann
>> <lars.wassermann at googlemail.com
>> <mailto:lars.wassermann at googlemail.com>> wrote:
>>
>>
>> Hello VM-dev,
>> I habe problems understanding what exactly happens when I step
>> through:
>> (ContextPart contextOn: Error do: []) halt
>> in a workspace.
>> When I step over the parenthesis in the debugger, everything
>> works kind of like expected (the context of contextOn:do:
>> misses one temporary but the returned value is what the
>> methods description promised). When I step into or through the
>> code, an error is raised when executing bytecode 54 of
>> #contextOn:do:, because the temps-array illegally has been
>> poped (by bytecode 53).
>>
>> My question now is:
>> Why does the normal vm not only break when runing this code,
>> but also return the correct value (the first element of the
>> last temporary).
>> I appended a graphic trying to visualize what happens up to
>> the miracle of three skipped bytecodes.
>>
>>
>> Ugh. I hate this method. Actually I hate ContextPart>>jump,
>> which is the really evil method.
>>
>> So let's take a look at the methods:
>>
>> ContextPasrt class>>contextOn: exceptionClass do: block
>> "Create an #on:do: context that is ready to return from executing
>> its receiver"
>>
>> | ctxt chain |
>> ctxt := thisContext.
>> [chain := thisContext sender cut: ctxt. ctxt jump]
>> on: exceptionClass
>> do: block.
>> "jump above will resume here without unwinding chain"
>> ^ chain
>>
>> answers a context that is an activation of on:do: ready to catch
>> the supplied exception. It is used by
>> ContextPart>>runUntilErrorOrReturnFrom: to insert an exception
>> handler for UnhandledError immediately beneath the current context
>> being executed by the debugger.
>>
>> The on:do: argument is a little unclear. It is easier to
>> understand if it reads
>>
>> ContextPasrt class>>contextOn: exceptionClass do: block
>> "Create an #on:do: context that is ready to return from executing
>> its receiver"
>>
>> | ctxt onDoContext |
>> ctxt := thisContext.
>> [onDoContext := thisContext sender.
>> onDoContext cut: ctxt.
>> ctxt jump]
>> on: exceptionClass
>> do: block.
>> "jump above will resume here without unwinding chain"
>> ^ onDoContext
>>
>> So the idea is to run until in the on:do:'s receiver (the
>> [onDoContext := ...] block), grab the sender (the on:do:
>> activation), remove intervening contexts (onDoContext cut: ctxt)
>> so that on:do:'s handlerActive doesn't get set, resume executing
>> in the contextOn:do: context (ctxt jump) and answer the onDoContext.
>>
>> The horror is in ContextPart>>jump:
>>
>> ContextPart>>jump
>> "Abandon thisContext and resume self instead (using the same
>> current process). You may want to save thisContext's sender
>> before calling this so you can jump back to it.
>> Self MUST BE a top context (ie. a suspended context or a abandoned
>> context that was jumped out of). A top context already has its
>> return value on its stack (see Interpreter>>primitiveSuspend and
>> other suspending primitives).
>> thisContext's sender is converted to a top context (by pushing a
>> nil return value on its stack) so it can be jump back to."
>>
>> | top |
>> "Make abandoned context a top context (has return value (nil)) so
>> it can be jumped back to"
>> thisContext sender push: nil.
>>
>> "Pop self return value then return it to self (since we jump to
>> self by returning to it)"
>> stackp = 0 ifTrue: [self stepToSendOrReturn].
>> stackp = 0 ifTrue: [self push: nil]. "must be quick return
>> self/constant"
>> top := self pop.
>> thisContext privSender: self.
>> ^ top
>>
>> This works by returning to self (a MethodContext), but return
>> pushes a result. So the code pops the top thing off the stack and
>> returns it. This is evil. If the stack is empty then this will
>> pop the receiver, return it and push it back in the return (!!).
>> If the stack contains only temps then the last temp is popped and
>> pushed by the return (!!). This is what you're seeing. The
>> stack's last temp is an indirection vector. It gets popped off
>> the stack by
>> top := self pop.
>> at which point any attempt to look at self's temps in a debugger
>> will cause an error because the indirection vector is no longer
>> there. It then gets pushed back by=
>> ^ top
>>
>> Horrible. It all depends on being able to create a temporary
>> invalid execution state in a context.
>>
>> A better way to do this is by process switch, avoiding the return.
>> For example, in my basic block profiler I handle an
>> unknownBytecode error caused by executing the unknownBytecode that
>> overwrites the bytecode at the beginning of each basic block,
>> replaces the unknownBytecode with the correct bytecode, and continues:
>>
>> ContextPart>>unusedBytecode
>> "Handle unusedBytecode by replacing the bytecode with the
>> correct one found in the coverage property and continuing.
>> Continue via wait/signal since return would push a result."
>> | coverage semaphore process |
>> self assert: (method at: pc) = method encoderClass unusedBytecode.
>> coverage := method propertyValueAt: #coverage.
>> self assert: coverage notNil.
>> self assert: (coverage includesKey: pc).
>> semaphore := Semaphore new.
>> process := Processor activeProcess.
>>
>> [method
>> at: pc
>> put: (coverage removeKey: pc).
>> process suspendedContext unwindTo: self.
>> process suspendedContext: self.
>> semaphore signal] fork.
>>
>> semaphore wait
>>
>> So that could become something like
>>
>> jump
>> "Abandon thisContext and resume self instead (using the same
>> current process). You may want to save thisContext's sender
>> before calling this so you can jump back to it.
>> Self MUST BE a top context (ie. a suspended context or a abandoned
>> context that was jumped out of). A top context already has its
>> return value on its stack (see Interpreter>>primitiveSuspend and
>> other suspending primitives)."
>> | semaphore process |
>> semaphore := Semaphore new.
>> process := Processor activeProcess.
>>
>> [process suspendedContext unwindTo: self.
>> process suspendedContext: self.
>> semaphore signal] fork.
>>
>> semaphore wait
>>
>> but perhaps the process switch will introduce other problems. I
>> don't know.
>>
>> Hope this helps.
>>
>>
>> I replicated the behavior in Squeak 4.0 and 4.4 images on
>> interpreter VMs 3.7.7, 4.10, and a newer cog VM.
>>
>>
>> A fix to the problem of stepping and comprehension as well as
>> the missing temp in the frame would be changing the method to:
>>
>> contextOn: exceptionClass do: block
>> "Create an #on:do: context that is ready to return
>> from executing its receiver"
>>
>> | ctxt chain |
>> ctxt := thisContext.
>> [chain := thisContext sender cut: ctxt.
>> ctxt push: nil. ctxt jump] on: exceptionClass do: block.
>> "jump above will resume here without unwinding chain"
>> ^ chain
>>
>> The difference is that after cutting off ctxt, we ensure that
>> it is a top context by pushing a meaningless value. But this
>> still doesn't help me understanding the original behaviour.
>>
>> Thank you and all the best
>> Lars
>>
>>
>> --
>> best,
>> Eliot
>
> --
> best,
> Eliot
More information about the Vm-dev
mailing list