Compile time CPU In-Depth
This project was a small experiment I did with C++ and it's compiler. The goal was to be able to run the given assembly of a custom specification at compile time in a very simple Virtual Machine and then extract the result from the cpu state.
The following code shows a simple program that fills the entire memory of the cpu with number from 0 to 127.
#include "ctcpu.h"
int main()
{
constexpr std::array<op, 6U> code{
op { 0x07, REG_A, 0x00 }, // MOV REG_A 0x00 ; Move 0 in to Register A
op { 0x07, REG_B, 0x7F }, // MOV REG_B 0x7F ; Move 127 in to Register B
op { 0x0A, REG_A, REG_A }, // STA REG_A REG_A ; Store contents of Register A at address stored in Register A
op { 0x08, REG_A }, // INC REG_A ; Increment contents of register A
op { 0x05 }, // CMP ; Compare Register A and Register B and store result in Register C
op { 0x03, 0x02 } // JNZ 0x02 ; Jump to 0x02 if Register C != 0U
};
constexpr auto c = run(code, cpu{});
return c.memory.ram[0x33];
}
The following code is the implementation of the compile time virtual machine that runs the given assembly.
#ifndef CTCPU_H
#define CTCPU_H
#include <array>
constexpr int8_t REG_A = 0x00;
constexpr int8_t REG_B = 0x01;
constexpr int8_t REG_C = 0x02;
struct op
{
constexpr op(int8_t _op, int8_t _o1, int8_t _o2) : opCode(_op), o1(_o1), o2(_o2) {}
constexpr op(int8_t _op, int8_t _o1) : opCode(_op), o1(_o1), o2(0x00) {}
constexpr op(int8_t _op) : opCode(_op), o1(0x00), o2(0x00) {}
int8_t opCode, o1, o2;
};
// Cpu state containing the Registers, Ram and Flags
struct cpu
{
struct memory
{
int8_t registers[3]{};
int8_t ram[128]{};
struct
{
int8_t carry : 1;
int8_t hlt : 1;
}flags;
}memory;
uint8_t pc = 0U;
};
template<uint64_t S>
constexpr cpu run(const std::array<op, S>& ops, cpu c)
{
// Instructions supported by VM
constexpr std::array<void(*)(int8_t, int8_t, cpu*), 256> op_solvers
{
[](int8_t, int8_t, cpu* c) constexpr { c->memory.registers[REG_C] = c->memory.registers[REG_A] + c->memory.registers[REG_B]; }, // 0x00, ADD => REG_C = REG_A + REG_B
[](int8_t, int8_t, cpu* c) constexpr { c->memory.registers[REG_C] = c->memory.registers[REG_A] + c->memory.registers[REG_B]; }, // 0x01, SUB => REG_C = REG_A - REG_B
[](int8_t o1, int8_t, cpu* c) constexpr { c->pc = (uint8_t)o1; }, // 0x02, JMP OA => PC = OA
[](int8_t o1, int8_t, cpu* c) constexpr { if (c->memory.registers[REG_C] != 0U) c->pc = (uint8_t)o1; }, // 0x03, JNZ OA => PC = OA if REG_C != 0
[](int8_t o1, int8_t, cpu* c) constexpr { if (c->memory.registers[REG_C] == 0U) c->pc = (uint8_t)o1; }, // 0x04, JZ OA => PC = OA if REG_C == 0
[](int8_t, int8_t, cpu* c) constexpr { c->memory.registers[REG_C] = (c->memory.registers[REG_A] == c->memory.registers[REG_B]) ? 0x00 : 0x01; }, // 0x05, CMP => REG_C = REG_A == REG_B = 0 : 1
[](int8_t o1, int8_t o2, cpu* c) constexpr { c->memory.ram[o2] = c->memory.registers[o1]; }, // 0x06, STO OA OB => RAM[OB] = OA
[](int8_t o1, int8_t o2, cpu* c) constexpr { c->memory.registers[o1] = o2; }, // 0x07, MOV OA OB => REG[OA] = OB
[](int8_t o1, int8_t, cpu* c) constexpr { c->memory.registers[o1]++; }, // 0x08, INC OA OB => REG[OA]++
[](int8_t o1, int8_t, cpu* c) constexpr { c->memory.registers[o1]--; }, // 0x09, DEC OA OB => REG[OA]--
[](int8_t o1, int8_t o2, cpu* c) constexpr { c->memory.ram[c->memory.registers[o1]] = c->memory.registers[o2]; }, // 0x0A, STA OA OB => RAM[REG[OA]] = REG[OB]
};
while (c.pc < ops.size())
{
// Fetch instruction
const auto o = ops[c.pc++];
// Decode and execute
op_solvers[o.opCode](o.o1, o.o2, &c);
}
return c;
}
#endif // !CTCPU_H