An interpreter
Now that we have our instruction set, we can execute a list of instructions. The basic process is one of fetching an instruction from the list, decoding it into an instruction name and destination and source registers, and then executing it. Do this in a loop and we’re up and running!
function interpret(instructions: string[]): number {
const registers: { [register: string ]: number } = {};
const memory: number[] = new Array(0x1000).fill(0);
const ip = 0;
while(true){
// Fetch.
const instruction = instructions[ip];
ip++;
// Decode.
const [operation, ...args] = instruction.split(/\s+/);
// Execute.
switch(operation){
case "halt":
return registers.r0;
case "add":
registers[args[0]] = registers[args[1]] + registers[args[2]];
break;
// ...
case "constant":
registers[args[0]] = parseInt(instructions[ip]);
ip++;
break;
// ...
case "jmp":
ip = registers[args[1]];
break;
default:
throw new Error(`invalid instruction ${dec.instr}`);
}
}
}
I’ve elided all kinds of error checking in the above – you can invent register names, you can get the number of arguments for an operation wrong, and so on. It’s an illustration, not an implementation!
By default we advance our instruction pointer by 1
; branch instructions change
this by setting ip
directly. The constant
instruction requires special handling,
to convert the next instruction to a number (an artifact of our especially
shitty approach to encoding and decoding instructions, which we will address shortly),
and to advance ip
again.
We’ve actually implemented a Harvard architecture here! Note
that our instructions live in one memory space instructions
, which we address by ip
,
and our data lives another memory space, memory
. This is once again down to our
shitty encoding.
Give it a try! You can edit the code below:
Ready.