(This page might be a bit more readable if you enable 1st-party CSS.)

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;
    // Fetch.
    const instruction = instructions[ip];

    // Decode.
    const [operation, ...args] = instruction.split(/\s+/);

    // Execute.
      case "halt":
        return registers.r0;
      case "add":
        registers[args[0]] = registers[args[1]] + registers[args[2]];
      // ...
      case "constant":
        registers[args[0]] = parseInt(instructions[ip]);
      // ...
      case "jmp":
        ip = registers[args[1]];
        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:

View source »
Next up: Encoding instructions »