Home Article List

Return Codes

I have kept these first two articles short because a lot of people have covered the subject matter already. Although maybe short-form reading is our solution to shortform brainrot media.

I have recently been working on a MIPS emulator. I thought I could talk about how return codes have worked out for this project in the places that I am using them. One such place is for the callbacks to the different CPU opcodes/functs. I use return codes to signal success, generic failure, and exceptions. Take a look at the following struct:

enum CPUCode
{
    CPU_SUCCESS,

    CPU_SIGNED_OVERFLOW,
    CPU_RESERVED_INSTRUCTION,
    CPU_COPROCESSOR_UNAVAILABLE,
    CPU_BREAKPOINT,
    CPU_ADDRESS_ERROR,
    CPU_BUS_ERROR,

    CPU_FATAL,
};

All callback functions for the various operations return one of these CPU codes.

CPU_SUCCESS CPU_FATAL Everything else (Exceptions)
Operation succeeded without encountering a fatal error or exception case. During operation, the system encountered a fatal internal error. During an operation, the system encountered a scenario that results in an exception as specified in this document (see section 2.1.8).

Many instructions will not raise an exception and have a very simple failure case. The addu opcode is a good example of this. It would only fail if given bad data internally, such as a null pointer to a vital object.

int i_addu(CPU *cpu, const u32 instruction)
{
     if (!cpu) return CPU_FATAL;
     u32 *r = cpu->registers;
     r[RD(instruction)] = r[RS(instruction)] + r[RT(instruction)];
     return CPU_SUCCESS;
}

The RD, RS, and RT macros just mask the instruction to isolate the desired fields. I’ll include them in a little section for instruction field macros. Anyway, look at the equivalent operation for signed integers:

int i_add(CPU *cpu, const u32 instruction)
{
     if (!cpu) return CPU_FATAL;
     const i32 rs = (i32)(cpu->registers[RS(instruction)]);
     const i32 rt = (i32)(cpu->registers[RT(instruction)]);
     const i64 rd = rs + rt;
     if (rd > INT32_MAX || rd < INT32_MIN) return CPU_INTEGER_OVERFLOW;
     cpu->registers[RD(instruction)] = (u32)rd;
     return CPU_SUCCESS;
}

In the specified case of signed overflow, the operation is not completed and we provide information of the exception to the caller. If I were to do something bigger with this VM in the future like, say, write some sort of language runtime of some sort, I could easily handle the various exceptions in any way that I see fit.

I can also form an error pipeline in a stacktrace-friendly manner. For memory units, I don’t have any exceptions, but I do have success and failure, so it serves me well for this example and could be extended easily in the future.

enum MemoryCode
{
    MEMORY_SUCCESS,
    MEMORY_FAIL
};

// Later

int memory_store_byte(Memory *mem, u8 byte, u64 address)
{
    if (mem == NULL
        || address >= mem->size)
        return MEMORY_FAIL;

    mem->data[address] = byte;
    return MEMORY_SUCCESS;
}

Now, my CPU can handle these errors as if they were coming from a helpful MMU. Maybe I’ll tap into the knowledge from my past self and throw a little logging in here to make it more helpful to my future debugging self.

int i_sb(CPU *cpu, u32 instruction)
{
     if (!cpu || !cpu->d_memory) return CPU_FATAL;
     const u32 base = RS(instruction);
     const i16 offset = IMM(instruction);
     if (memory_store_byte(cpu->d_memory, RT(instruction), base + offset) != MEMORY_SUCCESS)
     {
          dbg_printf("Memory failure on `sb` @ address %llu\n", base + offset);
          return CPU_FATAL;
     }
     return CPU_SUCCESS;
}

Instruction Field Macros

#define OPCODE(i) ((u8)((( (u32)i) >> 26) & 0x3F))
#define RS(i)     ((u8)((( (u32)i) >> 21) & 0x1F))
#define RT(i)     ((u8)((( (u32)i) >> 16) & 0x1F))
#define RD(i)     ((u8)((( (u32)i) >> 11) & 0x1F))
#define SHIFT(i)  ((u8)((( (u32)i) >> 6)  & 0x1F))
#define FUNCT(i)  ((u8)((( (u32)i)        & 0x3F)))
#define IMM(i)    ((u16)((((u32)i))       & 0xFFFF))
#define P_ADDR(i) ((u32)((((u32)i)        & 0x3FFFFFF)))

I overuse parenthesis and casting because I care.