You'd probably want to look at VMMaker. Its Interpreter class is the guy that executes a CompiledMethod's bytecodes, and will actually send the messages to your objects.
For instance, if you look at the bytecodes for Object>>respondsTo: you'll see
17 <70> self
18 <C7> send: class
19 <10> pushTemp: 0
20 <E0> send: canUnderstand:
21 <7C> returnTop
The Interpreter reads in a bytecode, looks up that bytecode in its BytecodeTable (initialised in Interpreter class>>initialiseBytecodeTable) and executes the appropriate method. So <70> (#pushReceiverByteCode) pushes self onto the Interpreter's internal stack. Then (#bytecodePrimClass) boils down to "find self's class". <10> (#pushTemporaryVariableBytecode) pushes the argument to #respondsTo: onto the stack. The interesting part happens with (#sendLiteralSelectorBytecode), which calls self normalSend
. #normalSend in turn figures out the class of the receiver (self class
in this case), and then calls self commonSend
, which finds the actual method we seek to run, and then runs it.
I'm a VM newbie; the above might not be the absolute best place to see the algorithm in action, etc., (or even the best explanation) but I hope it's a good place to start.
The algorithm used by the VM to actually send a message is as you outline in your question. The actual implementation of that algorithm's defined in Interpreter>>commonSend
. The lookup algorithm's in Interpreter>>lookupMethodInClass:
and the execution algorithm's in Interpreter>>internalExecuteNewMethod
.
The former works much as you describe:
- List item
- Try find the method in this class.
- If not found, look in the superclass.
- If this recursively fails, try find #doesNotUnderstand:
- If #doesNotUnderstand: doesn't exist anywhere in the class hierarchy, throw an error.
The latter works like this:
- If it's a primitive, run the primitive.
- If it's not, activate the new method (create a new activation record).
- (Check for interrupts.)