Bystroushaak's blog / English section / tinySelf / The "traits float" bug 2020/1

The "traits float" bug 2020/1

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 float (and int, 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 true, false and nil:

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: interpreter, self and parameters.

The second parameter obj which is mapped to self in the primitive function was in this case wrong, because it was resolved from traits float. slot_found_directly_in_obj was False, and slot had .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?


Tags

debugging, diy_programming_language, pypy, python, rpython, selflang, tinyself

Become a Patron