Here is update from the development of tinySelf, my pet programming language inspired by Smalltalk and Self.
When I was adding new methods to primitive types, I've encountered a nasty bug. The bug was caused by sharing primitive method objects for all instances of
bool and other primitive data types). By default, when
float object is created, it has a bunch of primitive slots that are pre-filled with primitive code objects. This is quite costly, both in terms of memory and time.
If you had multiple instances, whole structure would look like this:
Naturally, I've tried to add caching mechanism, that already works with other primitive objects, like
To my surprise, what happened is that when used for the first time, it worked as expected. But when you used the primitive method in another float object, the
self for primitive methods got redirected to the object used before. Interpreter uses this branch for handling primitive code:
elif slot.has_primitive_code: # primitives need "self" to be actually the object they are expecting, # for example for dicts it have to be dict, not some descendant # in the parent chain if not slot_found_directly_in_obj: obj = slot.scope_parent return_value = slot.primitive_code( self, obj, parameters ) if return_value is not None and self.process is not None: self.process.frame.push(return_value)
.primitive_code() method is a primitive (in rpython implemented) function expecting three parameters:
The second parameter
obj which is mapped to
self in the primitive function was in this case wrong, because it was resolved from
.scope_parent property set to last object in which context it was used. This happened to be primitive from previous call.
Fixing this was easy, but figuring where to find correct object which should be used as
self parameter for the primitive was hard. I've thought (and tried) about storing last primitive in call frame. I've explored a path to store
(obj, primitive) pairs on the stack, or creating linked primitive stacks in each frame, and several other ways. At the end, I've solved this by looking into the
.scope_parent of the
obj (which is at that time currently executed method). As the parameters are mapped into the
.scope_parent properties, this can be used as history. Code now looks like this:
elif slot.has_primitive_code: # primitives need "self" to be actually the object they are expecting, # for example for dicts it have to be dict, not some descendant # in the parent chain primitive_self = obj if not slot_found_directly_in_obj: primitive_self = self._find_primitives_self(primitive_self, slot) return_value = slot.primitive_code( self, # mapped to `interpreter` primitive_self, # mapped to `self` parameters # mapped to `params` ) if return_value is not None and self.process is not None: self.process.frame.push(return_value)
def _find_primitives_self(self, primitive_self, slot): """ In case of primitives that were resolved from traits, it is more complicated to find `self` parameter for the primitive function. See tinySelf.vm.primitives.add_primitive_fn.add_primitive_fn() and #132 for details. Args: primitive_self (Object): Candidate for the primitive's self. slot (Object): Method slot with primitive code. Returns: Object: Correct primitive self. """ # for slots resolved from the primitive classes (list, float, ..) if not isinstance(slot, PrimitiveCodeMethodObject): return slot.scope_parent # for slots resolved from traits go up the parent scope chain until you # find instance of the class from which this was resolved while not isinstance(primitive_self, slot.scope_parent.__class__): primitive_self = primitive_self.scope_parent if primitive_self is None or primitive_self is self.universe: return None return primitive_self
It's not the cleanest solution, but it is simple and it works.
On the difficulty
Although that written like this it may look simple, this wasn't easy debugging at all. It took a lot of tracing and prints and breakpoints and logging breakpoints and mental modeling to figure what is happening.
I've had really hard time when I tried to hold all the variables, different places, stack frames, contexts, objects and their copies and instances in my head. In the end, I was saved by outsourcing to the paper and resorting to visual memory. This sketch really helped me a lot:
What do you use for debugging? Do you have any killer technique?