Paul Bone

Type correctness in JIT code

Static type checking helps us be more confident that our software does what we think it does. But it can’t see everything, and this post was originally going to share a neat C++ feature that might have helped me be a little more confident about the code I’m writing. However just after I started writing this I found that it’s not necessary and there is (doh, I should have known) a nice C way to get the same check. However I still want to write it because it might be handy to remember this in the future.

I’m trying to add a counter to SpiderMonkey to count how many nursery, and tenured allocations there are. I’ve implemented this for the non-JIT paths and now it’s time to implement the same for the JIT paths. Here’s the code for the JIT during a nursery allocation:

add32(Imm32(1), AbsoluteAddress(zone->addressOfNurseryAllocCount()));

This code isn’t what runs at runtime (kinda) it’s part of the JIT and is executed when we want to generate code that performs a nursery allocation. In other words, it doesn’t perform the allocation itself, the machine code it generates will.

This causes the JIT to write instruction(s) (usually 1) that perform a 32-bit add. They add the immediate value 1, to the value contained at the address provided by the call zone->addressOfNurseryAllocCount(). This returns a pointer to a uint32_t value. However the AbsoluteAddress constructor will cast this to a void pointer, before writing the 32bit add instruction(s) using it.

This means that if in 6 months time I decide that we need 64 bit counters, or want to save so much memory that 16 bit counters would be better. That if the type of CompileZone::addressOfNurseryAllocCount() changed from uint32_t* to uint64_t* we’d have a problem (yes we would, popular platforms like x86 are little endian and will add the wrong bytes together).

So I wanted some kind of check here that if someone did make this change, they’d get a compiler error and change the add32 to an add64 for example. So I used:

static_assert(mozilla::IsSame<uint32_t*,
    decltype(zone->addressOfNurseryAllocCount())>::value,
    "JIT expects this to be a 32bit counter");
add32(Imm32(1), AbsoluteAddress(zone->addressOfNurseryAllocCount()));

Note that mozilla::IsSame is like std::is_same, it uses the template system to substitute in a different value for its value member depending on if the substituted types are the same. If they are, then the value is non-zero and the static_assert is accepted. But if the type of this function were to change then the assertion would fail, exactly what we want!

But there’s a simpler way. Just create a local variable of the desired type and assign to it first.

uint32_t *allocCount = zone->addressOfNurseryAllocCount();
add32(Imm32(1), AbsoluteAddress(allocCount));

The compilation will fail if the coercion implied by the assignment isn’t possible or safe. However, this won’t prevent all coercions (and hence my confusion), a uint32_t may be coerced to a uint64_t, but not a uint32_t* to a uint64_t*. So the simple solution is all we need here.

I’ve been through much of the existing JIT code today and made this type of improvement in many places Bug 1476500.

I wonder if dependent types can ensure that a code generator generates (more) type correct code?