11
\$\begingroup\$

(See the previous and initial iteration)

Now I have refactored the code in order to conform to the suggestions made in the answers to the first iteration. I have this:

toyvm.h:

#ifndef TOYVM_H
#define TOYVM_H

#include <stdbool.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>

enum {
    /* Arithmetics */
    ADD = 0x01,
    NEG = 0x02,
    MUL = 0x03,
    DIV = 0x04,
    MOD = 0x05,

    /* Conditionals */
    CMP = 0x10,
    JA  = 0x11,
    JE  = 0x12,
    JB  = 0x13,
    JMP = 0x14,

    /* Subroutines */
    CALL = 0x20,
    RET  = 0x21,

    /* Moving data */
    LOAD  = 0x30,
    STORE = 0x31,
    CONST = 0x32,

    /* Auxiliary */
    HALT = 0x40,
    INT  = 0x41,
    NOP  = 0x42,

    /* Stack */
    PUSH     = 0x50,
    PUSH_ALL = 0x51,
    POP      = 0x52,
    POP_ALL  = 0x53,
    LSP      = 0x54,

    /* Registers */
    REG1 = 0x00,
    REG2 = 0x01,
    REG3 = 0x02,
    REG4 = 0x03,

    /* Interupts */
    INTERRUPT_PRINT_INTEGER = 0x01,
    INTERRUPT_PRINT_STRING  = 0x02,

    /* Miscellaneous */
    N_REGISTERS = 4,

    OPCODE_MAP_SIZE = 256,
};

typedef struct VM_CPU {
    int32_t registers[N_REGISTERS];
    int32_t program_counter;
    int32_t stack_pointer;

    struct {
        uint8_t BAD_INSTRUCTION        : 1;
        uint8_t STACK_UNDERFLOW        : 1;
        uint8_t STACK_OVERFLOW         : 1;
        uint8_t INVALID_REGISTER_INDEX : 1;
        uint8_t BAD_ACCESS             : 1;
        uint8_t COMPARISON_BELOW       : 1;
        uint8_t COMPARISON_EQUAL       : 1;
        uint8_t COMPARISON_ABOVE       : 1;
    } status;
} VM_CPU;

typedef struct TOYVM {
    uint8_t* memory;
    int32_t  memory_size;
    int32_t  stack_limit;
    VM_CPU   cpu;
    size_t   opcode_map[OPCODE_MAP_SIZE];
} TOYVM;

/*******************************************************************************
* Initializes the virtual machine with RAM memory of length 'memory_size' and  *
* the stack fence at 'stack_limit'.
*******************************************************************************/
void InitializeVM(TOYVM* vm, int32_t memory_size, int32_t stack_limit);

/*******************************************************************************
* Writes 'size' bytes to the memory of the machine. The write begins from the  *
* beginning of the memory tape.                                                *
*******************************************************************************/
void WriteVMMemory(TOYVM* vm, uint8_t* mem, size_t size);

/*******************************************************************************
* Writes a single word 'value' (32-bit signed integer) at address 'address'.   *
*******************************************************************************/
void WriteWord(TOYVM* vm, int32_t address, int32_t value);

/*******************************************************************************
* Prints the status of the machine to stdout.                                  *
*******************************************************************************/
void PrintStatus(TOYVM* vm);

/*******************************************************************************
* Runs the virtual machine.                                                    *
*******************************************************************************/
void RunVM(TOYVM* vm);

#endif /* TOYVM_H */

toyvm.c:

#include "toyvm.h"
#include <stdbool.h>
#include <stdio.h>
#include <string.h>

typedef struct instruction {
    uint8_t   opcode;
    size_t    size;
    bool    (*execute)(TOYVM*);
} instruction;

/*******************************************************************************
* Return 'true' if the stack is empty.                                         *
*******************************************************************************/
static bool StackIsEmpty(TOYVM* vm)
{
    return vm->cpu.stack_pointer >= vm->memory_size;
}

/*******************************************************************************
* Return 'true' if the stack is full.                                          *
*******************************************************************************/
static bool StackIsFull(TOYVM* vm)
{
    return vm->cpu.stack_pointer <= vm->stack_limit;
}

/*******************************************************************************
* Returns the amount of free space in the stack in bytes.                      *
*******************************************************************************/
static int32_t GetAvailableStackSize(TOYVM* vm)
{
    return vm->cpu.stack_pointer - vm->stack_limit;
}

/*******************************************************************************
* Returns the number of bytes occupied by the stack.                           *
*******************************************************************************/
static int32_t GetOccupiedStackSize(TOYVM* vm)
{
    return vm->memory_size - vm->cpu.stack_pointer;
}

/*******************************************************************************
* Returns 'true' if the stack has enough room for pushing all registers to it. *
*******************************************************************************/
static bool CanPerformMultipush(TOYVM* vm)
{
    return GetAvailableStackSize(vm) >= sizeof(int32_t) * N_REGISTERS;
}

/*******************************************************************************
* Returns 'true' if the stack can provide data for all registers.              *
*******************************************************************************/
static bool CanPerformMultipop(TOYVM* vm)
{
    return GetOccupiedStackSize(vm) >= sizeof(int32_t) * N_REGISTERS;
}

/*******************************************************************************
* Returns 'true' if the instructoin does not run over the memory.              *
*******************************************************************************/
static bool InstructionFitsInMemory(TOYVM* vm, uint8_t opcode);

/*******************************************************************************
* Returns the length of the instruction with opcode 'opcode'.                  *
*******************************************************************************/
static size_t GetInstructionLength(TOYVM* vm, uint8_t opcode);

void InitializeVM(TOYVM* vm, int32_t memory_size, int32_t stack_limit)
{
    /* Make sure both 'memory_size' and 'stack_limit' are divisible by 4. */
    memory_size
    += sizeof(int32_t) - (memory_size % sizeof(int32_t));

    stack_limit
    += sizeof(int32_t) - (stack_limit % sizeof(int32_t));

    vm->memory              = calloc(memory_size, sizeof(uint8_t));
    vm->memory_size         = memory_size;
    vm->stack_limit         = stack_limit;
    vm->cpu.program_counter = 0;
    vm->cpu.stack_pointer   = (int32_t) memory_size;

    /***************************************************************************
    * Zero out all status flags.                                               *
    ***************************************************************************/
    vm->cpu.status.BAD_ACCESS       = 0;
    vm->cpu.status.COMPARISON_ABOVE = 0;
    vm->cpu.status.COMPARISON_EQUAL = 0;
    vm->cpu.status.COMPARISON_BELOW = 0;

    vm->cpu.status.BAD_INSTRUCTION        = 0;
    vm->cpu.status.INVALID_REGISTER_INDEX = 0;
    vm->cpu.status.STACK_OVERFLOW         = 0;
    vm->cpu.status.STACK_UNDERFLOW        = 0;

    /***************************************************************************
    * Zero out the registers and the map mapping opcodes to their respective   *
    * instruction descriptors.                                                 *
    ***************************************************************************/
    memset(vm->cpu.registers, 0, sizeof(int32_t) * N_REGISTERS);
    memset(vm->opcode_map, 0, sizeof(vm->opcode_map));

    /***************************************************************************
    * Build the opcode map.                                                    *
    ***************************************************************************/
    vm->opcode_map[ADD] = 1;
    vm->opcode_map[NEG] = 2;
    vm->opcode_map[MUL] = 3;
    vm->opcode_map[DIV] = 4;
    vm->opcode_map[MOD] = 5;

    vm->opcode_map[CMP] = 6;
    vm->opcode_map[JA]  = 7;
    vm->opcode_map[JE]  = 8;
    vm->opcode_map[JB]  = 9;
    vm->opcode_map[JMP] = 10;

    vm->opcode_map[CALL] = 11;
    vm->opcode_map[RET]  = 12;

    vm->opcode_map[LOAD]  = 13;
    vm->opcode_map[STORE] = 14;
    vm->opcode_map[CONST] = 15;

    vm->opcode_map[HALT] = 16;
    vm->opcode_map[INT]  = 17;
    vm->opcode_map[NOP]  = 18;

    vm->opcode_map[PUSH]     = 19;
    vm->opcode_map[PUSH_ALL] = 20;
    vm->opcode_map[POP]      = 21;
    vm->opcode_map[POP_ALL]  = 22;
    vm->opcode_map[LSP]      = 23;

}

void WriteVMMemory(TOYVM* vm, uint8_t* mem, size_t size)
{
    memcpy(mem, vm->memory, size);
}

static int32_t ReadWord(TOYVM* vm, int32_t address)
{
    uint8_t b1 = vm->memory[address];
    uint8_t b2 = vm->memory[address + 1];
    uint8_t b3 = vm->memory[address + 2];
    uint8_t b4 = vm->memory[address + 3];

    /* ToyVM is little-endian. */
    return (int32_t)((b4 << 24) | (b3 << 16) | (b2 << 8) | b1);
}

void WriteWord(TOYVM* vm, int32_t address, int32_t value)
{
    uint8_t b1 =  value & 0xff;
    uint8_t b2 = (value & 0xff00) >> 8;
    uint8_t b3 = (value & 0xff0000) >> 16;
    uint8_t b4 = (value & 0xff000000) >> 24;

    vm->memory[address]     = b1;
    vm->memory[address + 1] = b2;
    vm->memory[address + 2] = b3;
    vm->memory[address + 3] = b4;
}

static uint8_t ReadByte(TOYVM* vm, size_t address)
{
    return vm->memory[address];
}

/*******************************************************************************
* Pops a single word from the stack. Used by some instructions that implicitly *
* operate on stack.                                                            *
*******************************************************************************/
static int32_t PopVM(TOYVM* vm)
{
    if (StackIsEmpty(vm))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return 0;
    }

    int32_t word = ReadWord(vm, vm->cpu.stack_pointer);
    vm->cpu.stack_pointer += 4;
    return word;
}

/*******************************************************************************
* Pushes a single word to the stack. Used by some instructions. Used           *
* implicitly by some instructions.                                             *
*******************************************************************************/
static void PushVM(TOYVM* vm, uint32_t value)
{
    WriteWord(vm, vm->cpu.stack_pointer -= 4, value);
}

static bool IsValidRegisterIndex(uint8_t byte)
{
    switch (byte)
    {
        case REG1:
        case REG2:
        case REG3:
        case REG4:
            return true;
    }

    return false;
}

static int32_t GetProgramCounter(TOYVM* vm)
{
    return vm->cpu.program_counter;
}

static bool ExecuteAdd(TOYVM* vm)
{
    uint8_t source_register_index;
    uint8_t target_register_index;

    if (!InstructionFitsInMemory(vm, ADD))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    source_register_index = ReadByte(vm, GetProgramCounter(vm) + 1);
    target_register_index = ReadByte(vm, GetProgramCounter(vm) + 2);

    if (!IsValidRegisterIndex(source_register_index) ||
        !IsValidRegisterIndex(target_register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    vm->cpu.registers[target_register_index]
    += vm->cpu.registers[source_register_index];

    /* Advance the program counter past this instruction. */
    vm->cpu.program_counter += GetInstructionLength(vm, ADD);
    return false;
}

static bool ExecuteNeg(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, NEG))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    uint8_t register_index = ReadByte(vm, GetProgramCounter(vm) + 1);

    if (!IsValidRegisterIndex(register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    vm->cpu.registers[register_index] = -vm->cpu.registers[register_index];
    vm->cpu.program_counter += GetInstructionLength(vm, NEG);
    return false;
}

static bool ExecuteMul(TOYVM* vm)
{
    uint8_t source_register_index;
    uint8_t target_register_index;

    if (!InstructionFitsInMemory(vm, MUL))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    source_register_index = ReadByte(vm, GetProgramCounter(vm) + 1);
    target_register_index = ReadByte(vm, GetProgramCounter(vm) + 2);

    if (!IsValidRegisterIndex(source_register_index) ||
        !IsValidRegisterIndex(target_register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    vm->cpu.registers[target_register_index] *=
    vm->cpu.registers[source_register_index];
    /* Advance the program counter past this instruction. */
    vm->cpu.program_counter += GetInstructionLength(vm, MUL);
    return false;
}

static bool ExecuteDiv(TOYVM* vm)
{
    uint8_t source_register_index;
    uint8_t target_register_index;

    if (!InstructionFitsInMemory(vm, DIV))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    source_register_index = ReadByte(vm, GetProgramCounter(vm) + 1);
    target_register_index = ReadByte(vm, GetProgramCounter(vm) + 2);

    if (!IsValidRegisterIndex(source_register_index) ||
        !IsValidRegisterIndex(target_register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    vm->cpu.registers[target_register_index] /=
    vm->cpu.registers[source_register_index];
    /* Advance the program counter past this instruction. */
    vm->cpu.program_counter += GetInstructionLength(vm, DIV);
    return false;
}

static bool ExecuteMod(TOYVM* vm)
{
    uint8_t source_register_index;
    uint8_t target_register_index;

    if (!InstructionFitsInMemory(vm, MOD))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    source_register_index = ReadByte(vm, GetProgramCounter(vm) + 1);
    target_register_index = ReadByte(vm, GetProgramCounter(vm) + 2);

    if (!IsValidRegisterIndex(source_register_index) ||
        !IsValidRegisterIndex(target_register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    vm->cpu.registers[target_register_index] =
    vm->cpu.registers[source_register_index] %
    vm->cpu.registers[target_register_index];

    /* Advance the program counter past this instruction. */
    vm->cpu.program_counter += GetInstructionLength(vm, MOD);
    return false;
}

static bool ExecuteCmp(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, CMP))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    uint8_t register_index_1 = ReadByte(vm, GetProgramCounter(vm) + 1);
    uint8_t register_index_2 = ReadByte(vm, GetProgramCounter(vm) + 2);

    if (!IsValidRegisterIndex(register_index_1) ||
        !IsValidRegisterIndex(register_index_2))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    int32_t register_1 = vm->cpu.registers[register_index_1];
    int32_t register_2 = vm->cpu.registers[register_index_2];

    if (register_1 < register_2)
    {
        vm->cpu.status.COMPARISON_ABOVE = 0;
        vm->cpu.status.COMPARISON_EQUAL = 0;
        vm->cpu.status.COMPARISON_BELOW = 1;
    }
    else if (register_1 > register_2)
    {
        vm->cpu.status.COMPARISON_ABOVE = 1;
        vm->cpu.status.COMPARISON_EQUAL = 0;
        vm->cpu.status.COMPARISON_BELOW = 0;
    }
    else
    {
        vm->cpu.status.COMPARISON_ABOVE = 0;
        vm->cpu.status.COMPARISON_EQUAL = 1;
        vm->cpu.status.COMPARISON_BELOW = 0;
    }

    vm->cpu.program_counter += GetInstructionLength(vm, CMP);
    return false;
}

static bool ExecuteJumpIfAbove(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, JA))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    if (vm->cpu.status.COMPARISON_ABOVE)
    {
        vm->cpu.program_counter = ReadWord(vm, GetProgramCounter(vm) + 1);
    }
    else
    {
        vm->cpu.program_counter += GetInstructionLength(vm, JA);
    }

    return false;
}

static bool ExecuteJumpIfEqual(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, JE))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    if (vm->cpu.status.COMPARISON_EQUAL)
    {
        vm->cpu.program_counter = ReadWord(vm, GetProgramCounter(vm) + 1);
    }
    else
    {
        vm->cpu.program_counter += GetInstructionLength(vm, JE);
    }

    return false;
}

static bool ExecuteJumpIfBelow(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, JB))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    if (vm->cpu.status.COMPARISON_BELOW)
    {
        vm->cpu.program_counter = ReadWord(vm, GetProgramCounter(vm) + 1);
    }
    else
    {
        vm->cpu.program_counter += GetInstructionLength(vm, JB);
    }

    return false;
}

static bool ExecuteJump(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, JMP))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    vm->cpu.program_counter = ReadWord(vm, GetProgramCounter(vm) + 1);
    return false;
}

static bool ExecuteCall(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, CALL))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    if (GetAvailableStackSize(vm) < 4)
    {
        vm->cpu.status.STACK_OVERFLOW = 1;
        return true;
    }

    /* Save the return address on the stack. */
    uint32_t address = ReadWord(vm, GetProgramCounter(vm) + 1);
    PushVM(vm, (uint32_t)(GetProgramCounter(vm) +
                          GetInstructionLength(vm, CALL)));
    /* Actual jump to the subroutine. */
    vm->cpu.program_counter = address;
    return false;
}

static bool ExecuteRet(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, RET))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    if (StackIsEmpty(vm))
    {
        vm->cpu.status.STACK_UNDERFLOW = 1;
        return true;
    }

    vm->cpu.program_counter = PopVM(vm);
    return false;
}

static bool ExecuteLoad(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, LOAD))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    uint8_t register_index = ReadByte(vm, GetProgramCounter(vm) + 1);

    if (!IsValidRegisterIndex(register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    uint32_t address = ReadWord(vm, GetProgramCounter(vm) + 2);
    vm->cpu.registers[register_index] = ReadWord(vm, address);
    vm->cpu.program_counter += GetInstructionLength(vm, LOAD);
    return false;
}

static bool ExecuteStore(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, STORE))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    uint8_t register_index = ReadByte(vm, GetProgramCounter(vm) + 1);

    if (!IsValidRegisterIndex(register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    uint32_t address = ReadWord(vm, GetProgramCounter(vm) + 2);
    WriteWord(vm, address, vm->cpu.registers[register_index]);
    vm->cpu.program_counter += GetInstructionLength(vm, STORE);
    return false;
}

static bool ExecuteConst(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, CONST))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    uint8_t register_index = ReadByte(vm, GetProgramCounter(vm) + 1);
    int32_t datum = ReadWord(vm, GetProgramCounter(vm) + 2);

    if (!IsValidRegisterIndex(register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    vm->cpu.registers[register_index] = datum;
    vm->cpu.program_counter += GetInstructionLength(vm, CONST);
    return false;
}

static void PrintString(TOYVM* vm, uint32_t address)
{
    printf("%s", (const char*)(&vm->memory[address]));
}

static bool ExecuteInterrupt(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, INT))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    uint8_t interrupt_number = ReadByte(vm, GetProgramCounter(vm) + 1);

    if (StackIsEmpty(vm))
    {
        vm->cpu.status.STACK_UNDERFLOW = 1;
        return true;
    }

    switch (interrupt_number)
    {
        case INTERRUPT_PRINT_INTEGER:
            printf("%d", PopVM(vm));
            break;

        case INTERRUPT_PRINT_STRING:
            PrintString(vm, PopVM(vm));
            break;

        default:
            return true;
    }

    vm->cpu.program_counter += GetInstructionLength(vm, INT);
    return false;
}

static bool ExecutePush(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, PUSH))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    if (StackIsFull(vm))
    {
        return true;
    }

    uint8_t register_index = ReadByte(vm, GetProgramCounter(vm) + 1);

    if (!IsValidRegisterIndex(register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    WriteWord(vm,
              vm->cpu.stack_pointer - 4,
              vm->cpu.registers[register_index]);

    vm->cpu.stack_pointer -= 4;
    vm->cpu.program_counter += GetInstructionLength(vm, PUSH);
    return false;
}

static bool ExecutePushAll(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, PUSH_ALL))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    if (!CanPerformMultipush(vm))
    {
        vm->cpu.status.STACK_OVERFLOW = 1;
        return true;
    }

    WriteWord(vm, vm->cpu.stack_pointer -= 4, vm->cpu.registers[REG1]);
    WriteWord(vm, vm->cpu.stack_pointer -= 4, vm->cpu.registers[REG2]);
    WriteWord(vm, vm->cpu.stack_pointer -= 4, vm->cpu.registers[REG3]);
    WriteWord(vm, vm->cpu.stack_pointer -= 4, vm->cpu.registers[REG4]);
    vm->cpu.program_counter += GetInstructionLength(vm, PUSH_ALL);
    return false;
}

static bool ExecutePop(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, POP))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    if (StackIsEmpty(vm))
    {
        return true;
    }

    uint8_t register_index = ReadByte(vm, GetProgramCounter(vm) + 1);

    if (!IsValidRegisterIndex(register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    int32_t datum = ReadWord(vm, GetProgramCounter(vm) + 2);
    vm->cpu.registers[register_index] = datum;
    vm->cpu.stack_pointer += 4;
    vm->cpu.program_counter += GetInstructionLength(vm, POP);
    return false;
}

static bool ExecutePopAll(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, POP_ALL))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    if (!CanPerformMultipop(vm))
    {
        vm->cpu.status.STACK_UNDERFLOW = 1;
        return true;
    }

    vm->cpu.registers[REG4] = ReadWord(vm, vm->cpu.stack_pointer);
    vm->cpu.registers[REG3] = ReadWord(vm, vm->cpu.stack_pointer + 4);
    vm->cpu.registers[REG2] = ReadWord(vm, vm->cpu.stack_pointer + 8);
    vm->cpu.registers[REG1] = ReadWord(vm, vm->cpu.stack_pointer + 12);
    vm->cpu.stack_pointer += 16;
    vm->cpu.program_counter += GetInstructionLength(vm, POP_ALL);
    return false;
}

static bool ExecuteLSP(TOYVM* vm)
{
    if (!InstructionFitsInMemory(vm, LSP))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    uint8_t register_index = ReadByte(vm, GetProgramCounter(vm) + 1);

    if (!IsValidRegisterIndex(register_index))
    {
        vm->cpu.status.INVALID_REGISTER_INDEX = 1;
        return true;
    }

    vm->cpu.registers[register_index] = vm->cpu.stack_pointer;
    vm->cpu.program_counter += GetInstructionLength(vm, LSP);
    return false;
}

static bool ExecuteNop(TOYVM* vm) {
    if (!InstructionFitsInMemory(vm, NOP))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    vm->cpu.program_counter += GetInstructionLength(vm, NOP);
    return false;
}

static bool ExecuteHalt(TOYVM* vm) {
    return true;
}

void PrintStatus(TOYVM* vm)
{
    printf("BAD_INSTRUCTION       : %d\n", vm->cpu.status.BAD_INSTRUCTION);
    printf("STACK_UNDERFLOW       : %d\n", vm->cpu.status.STACK_UNDERFLOW);
    printf("STACK_OVERFLOW        : %d\n", vm->cpu.status.STACK_OVERFLOW);
    printf("INVALID_REGISTER_INDEX: %d\n",
           vm->cpu.status.INVALID_REGISTER_INDEX);

    printf("BAD_ACCESS            : %d\n", vm->cpu.status.BAD_ACCESS);
    printf("COMPARISON_ABOVE      : %d\n", vm->cpu.status.COMPARISON_ABOVE);
    printf("COMPARISON_EQUAL      : %d\n", vm->cpu.status.COMPARISON_EQUAL);
    printf("COMPARISON_BELOW      : %d\n", vm->cpu.status.COMPARISON_BELOW);
}

const instruction instructions[] = {
    { 0,        0, NULL       },
    { ADD,      3, ExecuteAdd },
    { NEG,      2, ExecuteNeg },
    { MUL,      3, ExecuteMul },
    { DIV,      3, ExecuteDiv },
    { MOD,      3, ExecuteMod },

    { CMP,      3, ExecuteCmp },
    { JA,       5, ExecuteJumpIfAbove },
    { JE,       5, ExecuteJumpIfEqual },
    { JB,       5, ExecuteJumpIfBelow },
    { JMP,      5, ExecuteJump },

    { CALL,     5, ExecuteCall },
    { RET,      1, ExecuteRet },

    { LOAD,     6, ExecuteLoad },
    { STORE,    6, ExecuteStore },
    { CONST,    6, ExecuteConst },

    { HALT,     1, ExecuteHalt },
    { INT,      2, ExecuteInterrupt },
    { NOP,      1, ExecuteNop },

    { PUSH,     2, ExecutePush },
    { PUSH_ALL, 1, ExecutePushAll },
    { POP,      2, ExecutePop },
    { POP_ALL,  1, ExecutePopAll },
    { LSP,      2, ExecuteLSP }
};

static size_t GetInstructionLength(TOYVM* vm, uint8_t opcode)
{
    size_t index = vm->opcode_map[opcode];
    return instructions[index].size;
}

/*******************************************************************************
 * Checks that an instruction fits entirely in the memory.                      *
 *******************************************************************************/
static bool InstructionFitsInMemory(TOYVM* vm, uint8_t opcode)
{
    size_t instruction_length = GetInstructionLength(vm, opcode);
    return vm->cpu.program_counter + instruction_length <= vm->memory_size;
}

void RunVM(TOYVM* vm)
{
    while (true)
    {
        int32_t program_counter = GetProgramCounter(vm);

        if (program_counter < 0 || program_counter >= vm->memory_size)
        {
            vm->cpu.status.BAD_ACCESS = 1;
            return;
        }

        uint8_t opcode = vm->memory[program_counter];
        size_t index = vm->opcode_map[opcode];

        if (index == 0)
        {
            vm->cpu.status.BAD_INSTRUCTION = 1;
            return;
        }

        bool (*opcode_exec)(TOYVM*) =
        instructions[index].execute;

        if (opcode_exec(vm))
        {
            return;
        }
    }
}

main.c:

#include <stdio.h>
#include "toyvm.h"

static size_t getFileSize(FILE* file)
{
    long int original_cursor = ftell(file);
    fseek(file, 0L, SEEK_END);
    size_t size = ftell(file);
    fseek(file, original_cursor, SEEK_SET);
    return size;
}

int main(int argc, const char * argv[]) {
    if (argc != 2)
    {
        puts("Usage: toy FILE.brick\n");
        return 0;
    }

    FILE* file = fopen(argv[1], "r");
    size_t file_size = getFileSize(file);

    TOYVM vm;
    InitializeVM(&vm, 10000, 5000);

    fread(vm.memory, 1, file_size, file);
    fclose(file);

    RunVM(&vm);

    if (vm.cpu.status.BAD_ACCESS
        || vm.cpu.status.BAD_INSTRUCTION
        || vm.cpu.status.INVALID_REGISTER_INDEX
        || vm.cpu.status.STACK_OVERFLOW
        || vm.cpu.status.STACK_UNDERFLOW)
    {
        PrintStatus(&vm);
    }
}

Also, you can find the FizzBuzz program for ToyVM here; run as toy fizzbuzz.brick.

Please, tell me anything that comes to mind.

\$\endgroup\$
3
  • \$\begingroup\$ You might find this vm sketch interesting and/or useful. It packs the opcode functions in an X-macro table and automatically generates corresponding functions, enums, and manifest function names for the function-pointer table. \$\endgroup\$ Commented Mar 15, 2016 at 7:42
  • \$\begingroup\$ Umm, so you sort of code in C macros??? \$\endgroup\$
    – coderodde
    Commented Mar 21, 2016 at 15:54
  • \$\begingroup\$ Yes, I'm pretty much addicted to macros. Here's an SO question all about X-Macros. \$\endgroup\$ Commented Mar 21, 2016 at 22:49

1 Answer 1

11
\$\begingroup\$

I see some things that may help you improve your code.

Don't leak memory

The code for InitializeVM allocate memory for the vm->memory member. Every allocation should have a corresponding free, so I would suggest adding this function to the interface and then calling it at the end of main:

void DestroyVM(TOYVM* vm) {
    free(vm->memory);
}

Check return values

The call to calloc can fail and when it does, the only indication is that the value returned is equal to NULL. For that reason, you must check the value for a robust program. The same is true of other functions such as fopen.

Think about signed versus unsigned

There are several values in the virtual machine, such as program_counter and stack_pointer and memory_size which are declared to be int32_t types. However, does it actually make sense for any of these to be negative numbers? I suspect not, so I would advise making them all uint32_t instead. Arguably, this would apply to the return values of GetAvailableStackSize(), GetOccupiedStackSize(), etc. as well.

Return something useful from functions

The RunVM() function is a void function right now, but it would probably be more useful to have it return true in the case of an error or false otherwise. This would allow for simple error checking after the VM has run.

Simplify your code

The RunVM() function currently has these line:

bool (*opcode_exec)(TOYVM*) = instructions[index].execute;

if (opcode_exec(vm))  

There isn't really a need to create a separate variable here. This could instead be written as a single line:

    if (instructions[index].execute(vm))

Consolidate data tables

The decoding of opcodes uses two structures; one is the instructions array which is ordered by opcode numerical value, and the other is opcode_map which is an array of indices into the instructions array. There's nothing inherently wrong with that, but since it does not appear that the instructions are intended to be dynamically created or assigned by users of the program, these structures could either be consolidated into a single static structure, or you could keep them separate but statically create the opcode_map structure rather that initializing it at runtime.

Don't Repeat Yourself

In almost every ExecuteXXX function, the code begins with a memory check and ends by updating the program counter. For example, the NOP instruction:

static bool ExecuteNop(TOYVM* vm) {
    if (!InstructionFitsInMemory(vm, NOP))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }

    vm->cpu.program_counter += GetInstructionLength(vm, NOP);
    return false;
}

This repetition could be factored out:

static bool ExecuteWrapper(TOYVM* vm, uint8_t opcode, bool (*execute)(TOYVM*), bool isNotJump) {
    if (!InstructionFitsInMemory(vm, opcode))
    {
        vm->cpu.status.BAD_ACCESS = 1;
        return true;
    }
    if (execute(vm)) {
        return true;
    }
    if (isNotJump) {
        vm->cpu.program_counter += GetInstructionLength(vm, opcode);
    }
    return false;
}

Then the instruction struct could have an additional boolean member isNotJump which would, of course, say whether the instruction might advance the program counter to something other than the next instruction. Calling would then look like this:

bool retval = ExecuteWrapper(vm, opcode, 
        instructions[index].execute, instructions[index].isNotJump);

Separate enums

The enums in toyvm.h are not all mutually exclusive values for the same concept which is a little confusing. I would recommend having the instruction values as one enum and then only group related values in other enums. That makes it clear that each enum is a collection of something rather than just a random assortment of constants.

\$\endgroup\$
9
  • \$\begingroup\$ While "free all your memory" sounds nice, it's a really bad idea for large programs. There's just no fun in watching a program spend several minutes paging in all its old memory to free it, just when it was told that the whole process is getting torn down. The operating system will do the work for you anyhow and much more efficient. Personally not a big fan of all the unnecessary bugs that using unsigned easily introduces, but that's more arguable. \$\endgroup\$
    – Voo
    Commented Mar 12, 2016 at 21:32
  • \$\begingroup\$ @Voo: the problem you mention is due to a particular operating system's design errors rather than an intrinsic problem with the idea of not leaking memory. Regardless of the underlying operating system, however, if memory is allocated, it should be at least possible to free it via the system interface. Either the calling program should allocate and free or the called program should; requiring a mix of "both" is usually a sign of a design error. \$\endgroup\$
    – Edward
    Commented Mar 12, 2016 at 23:01
  • \$\begingroup\$ And what exactly would the design flaw in Windows be here? Considering that the only thing the kernel does in this scenario is page in memory the application asked for I'm really not sure what you're getting at. Particularly since the same effect can be observed under Linux with memory constrained programs. \$\endgroup\$
    – Voo
    Commented Mar 12, 2016 at 23:32
  • \$\begingroup\$ This does not seem to be a "memory constrained program" so I don't see how your comment relates. \$\endgroup\$
    – Edward
    Commented Mar 13, 2016 at 0:26
  • \$\begingroup\$ So there's no design error in any particular OS any more I gather then? And "memory constrained" is not an attribute of any particular program but the environment it is run in (a program may run great on a normal desktop, but can easily be memory constrained on an embedded device). In any case learning bad habits just because it doesn't matter strikes me as a weird position to take. \$\endgroup\$
    – Voo
    Commented Mar 13, 2016 at 15:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.