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