On Tue, Jul 1, 2008 at 2:23 PM, tim Rowledge <tim@rowledge.org> wrote:

I did primitive error codes at Cadence and they'll very probably b making it into Cog real soon now.  They're simpler than VisualWorks', being only symbols.  So extracting more information, such as a related error code requires a subsequent call.  But I think the work I'm doing right now on eliminating pushRemappableOop:/popRemappableOop will enable me to have a structured object with a name and parameters, which is more generally useful.

So.... what is the anticipated way that will work? I'm not sure I see the need for a two step process anyway.

The version we did at Interval in 98 was very simple, providing a call for prims to stuff some object (we used SmallInts in prcatice, but anything was allowed) into the first slot in the context that got activated on the fail. We extended the primitive declaration pragma to allow optional naming of the tempvar - default was 'errorValue' I think - and that was it. Code following the prim call could use or ignore the temp.

The compiler is modified to recognise <primitive: integer errorCode: identifier> and <primitive: identifier module: identifier errorCode: identifier> and convert this into one additional temp and generate a long storeTemp as the first instruction of the method.

The VM is modified to, on primitive failure, check for the  long storeTemp and stores the error code if it sees it.

Old images lack the storeTemp so the VM does not store the value.  Old VMs do perform the storeTemp, but because the storeTemp is storing into the stack top it is effectively a noop.  e.g. if the primitive looks like:

primitiveAdoptInstance: anObject
<primitive: 160 error: ec>
ec == #'no modification' ifTrue:
signal: self
message: (Message
selector: #primitiveAdoptInstance:
arguments: {anObject})].
self primitiveFailed

then the header says the method has 2 temps (anObject & ec).  The VM initializes the context with stackp pointing at the second temp (ec).  The merthod starts with a long storeTemp: 2.  So the nil ec gets stored into itself.  The compiler generates a long storeTemp to make it quicker to check for the method starting with a storeTemp.

A nice thing is that the code is forwards and backewards compatible.  One can use the VM to run older images.  One can run images that contain the primitive error code on older VMs, where one simply gets a nil error code on primitive failure.

Sure, if the temp is used by return-code-unaware methods in an older image then it can't make much difference because it would be assumed nil as normal. Running new images on older VMs isn't something we've ever much bothered with for Squeak so don't worry about it.

I'm going to guess that "work I'm doing right now on eliminating pushRemappableOop:/popRemappableOop" relates to getting allocations out of primitives as much as possible, right?  That would be nice.

No.  This relates to not having to use pushRemappableOop:/popRemappableOop because it is extremely error-prone.  For example can you spot the bug in this:

| performSelector newReceiver selectorIndex lookupClass performMethod |
performSelector := messageSelector.
performMethod := newMethod.
messageSelector := self stackValue: argumentCount - 1.
newReceiver := self stackValue: argumentCount.

"NOTE: the following lookup may fail and be converted to #doesNotUnderstand:, so we must adjust argumentCount and slide args now, so that would work."

"Slide arguments down over selector"
argumentCount := argumentCount - 1.
selectorIndex := self stackPointerIndex - argumentCount.
transfer: argumentCount
fromIndex: selectorIndex + 1
ofObject: activeContext
toIndex: selectorIndex
ofObject: activeContext.
self pop: 1.
lookupClass := self fetchClassOf: newReceiver.
self findNewMethodInClass: lookupClass.

"Only test CompiledMethods for argument count - other objects will have to take their chances"
(self isCompiledMethod: newMethod)
ifTrue: [self success: (self argumentCountOf: newMethod) = argumentCount].

self successful
ifTrue: [self executeNewMethodFromCache.
"Recursive xeq affects successFlag"
self initPrimCall]
ifFalse: ["Slide the args back up (sigh) and re-insert the 
            selector. "

1 to: argumentCount do: [:i | self
storePointer: argumentCount - i + 1 + selectorIndex
ofObject: activeContext
withValue: (self fetchPointer: argumentCount - i + selectorIndex ofObject: activeContext)].
self unPop: 1.
self storePointer: selectorIndex
ofObject: activeContext
withValue: messageSelector.
argumentCount := argumentCount + 1.
newMethod := performMethod.
messageSelector := performSelector]

Don't look further.  try and spot the bug first.

It starts with
    performSelector := messageSelector.
performMethod := newMethod.
and ends with
            newMethod := performMethod.
messageSelector := performSelector
which is only executed if there is a doesNotUnderstand:.

A doesNotUnderstand: may cause a GC in createActualMessage as it creates the message argument for doesNotUnderstand:.  So this bug only bytes if a perform is not understood when memory is on the verge of exhaustion.

In the context-to-stack-mapping VM I'm working on the VM may do up to a page worth of context allocations on e.g. trying to store the sender of a context.  Rather than try and plug all the holes it is much safer to restrict garbage collection to happening between bytecodes (probably on sends and backward branches).  To do this the GC has to maintain a reserve for the VM which is about the size of two stack pages, or probably 2k bytes, and the VM has to defer incremental collections from allocations until the send or backward branch following.