December of Rust 2021, Part 1: A Little Computer

In the beginning of December I read Andrei Ciobanu’s Writing a simple 16-bit VM in less than 125 lines of C. Now, I’ve been interested in virtual machines and emulators for a long time, and I work tangential to VMs as part of my day job as a JavaScript engine developer for Igalia. I found this post really interesting because, well, it does what it says on the tin: A simple VM in less than 125 lines of C.

Readers of this blog, if I have any left at this point, might remember that in December 2020 I did a series of posts solving that year’s Advent of Code puzzles in order to try to teach myself how to write programs in the Rust programming language. I did say last year that if I were to do these puzzles again, I would do them at a slower pace and wouldn’t blog about them. Indeed, I started again this year, but it just wasn’t as interesting, having already gone through the progression from easy to hard puzzles and now having some idea already of the kinds of twists that they like to do in between the first and second halves of each puzzle.

So, instead of puzzles, I decided to see if I could write a similarly simple VM in Rust this December, as a continuation of my Rust learning exercise last year1.

Andrei Ciobanu’s article, as well as some other articles he cites, write VMs that simulate the LC-3 architecture2. What I liked about this one is that it was really concise and no-nonsense, and did really well at illustrating how a VM works. There are already plenty of other blog posts and GitHub repositories that implement an LC-3 VM in Rust, and I can’t say I didn’t incorporate any ideas from them, but I found many of them to be a bit verbose. I wanted to see if I could create something in the same concise spirit as Andrei’s, but still idiomatic Rust.

Over the course of a couple of weeks during my end-of-year break from work, I think I succeeded somewhat at that, and so I’m going to write a few blog posts about it.

About the virtual machine

This post is not a tutorial about virtual machines. There are already plenty of those, and Andrei’s article is already a great one so it doesn’t make sense for me to duplicate it. Instead, In this section I’ll note some things about the LC-3 architecture before we start.

First of all, it has a very spartan instruction set. Each instruction is 16 bits, and there are no variable length instructions. The opcode is packed in the topmost 4 bits of each word. That means there are at most 16 instructions. And one opcode (1101) is not even used!

Only three instructions are arithmetic-type ones: addition, bitwise AND, and bitwise NOT. If you’re used to x86 assembly language you’ll notice that other operations like subtraction, multiplication, bitwise OR, are missing. We only need these three to do all the other operations in 2’s-complement arithmetic, although it is somewhat tedious! As I started writing some LC-3 assembly language to test the VM, I learned how to implement some other arithmetic operations in terms of ADD, AND, and NOT.3 I’ll go into this in a following post.

The LC-3 does not have a stack. All the operations take place in registers. If you are used to thinking in terms of a stack machine (for example, SpiderMonkey is one), this takes some getting used to.

First steps

I started out by trying to port Andrei’s C code to Rust code in the most straightforward way possible, not worrying about whether it was idiomatic or not.

The first thing I noticed is that whereas in C it’s customary to use a mutable global, such as reserving storage for the VM’s memory and registers at the global level with declarations such as uint16_t mem[UINT16_MAX] = {0};, the Rust compiler makes this very difficult. You can use a mutable static variable, but accessing it needs to be marked as unsafe. In this way, the Rust compiler nudges you to encapsulate the storage inside a class:

struct VM {
    mem: [u16; MEM_SIZE],
    reg: [u16; NREGS],
    running: bool,
}

Next we write functions to access the memory. In the C code these are:

static inline uint16_t mr(uint16_t address) { return mem[address];  }
static inline void mw(uint16_t address, uint16_t val) { mem[address] = val; }

In Rust, we have to cast the address to a usize, since usize is the type that we index arrays with:

#[inline]
fn ld(&mut self, addr: u16) -> u16 {
    self.mem[addr as usize]
}

#[inline]
fn st(&mut self, addr: u16, val: u16) {
    self.mem[addr as usize] = val;
}

(I decide to name them ld and st for “load” and “store” instead of mr and mw, because the next thing I do is write similar functions for reading and writing the VM’s registers, which I’ll call r and rw for “register” and “register write”. These names look less similar, so I find that makes the code more readable yet still really concise.)

The next thing in the C code is a bunch of macros that do bit-manipulation operations to unpack the instructions. I decide to turn these into #[inline] functions in Rust. For example,

#define OPC(i) ((i)>>12)
#define FIMM(i) ((i>>5)&1)

from the C code, become, in Rust,

#[inline] #[rustfmt::skip] fn opc(i: u16) -> u16 { i >> 12 }
#[inline] #[rustfmt::skip] fn fimm(i: u16) -> bool { (i >> 5) & 1 != 0 }

I put #[rustfmt::skip] because I think it would be nicer if the rustfmt tool would allow you to put super-trivial functions on one line, so that they don’t take up more visual space than they deserve.

You might think that the return type of opc should be an enum. I originally tried making it that way, but Rust doesn’t make it very easy to convert between enums and integers. The num_enum crate provides a way to do this, but I ended up not using it, as you will read below.

We also need a way to load and run programs in LC-3 machine code. I made two methods of VM patterned after the ld_img() and start() functions from the C code.

First I’ll talk about ld_img(). What I really wanted to do is read the bytes of the file directly into the self.mem array, without copying, as the C code does. This is not easy to do in Rust. Whereas in C all pointers are essentially pointers to byte arrays in the end, this is not the case in Rust. It’s surprisingly difficult to express that I want to read u16s into an array of u16s! I finally found a concise solution, using both the byteorder and bytemuck crates. For this to work, you have to import the byteorder::ReadBytesExt trait into scope.

pub fn ld_img(&mut self, fname: &str, offset: u16) -> io::Result<()> {
    let mut file = fs::File::open(fname)?;
    let nwords = file.metadata()?.len() as usize / 2;
    let start = (PC_START + offset) as usize;
    file.read_u16_into::<byteorder::NetworkEndian>(bytemuck::cast_slice_mut(
        &mut self.mem[start..(start + nwords)],
    ))
}

What this does is read u16s, minding the correct byte order, into an array of u8. But we have an array of u16 that we want to store it in, not u8. So bytemuck::cast_slice_mut() treats the &mut [u16] slice as a &mut [u8] slice, essentially equivalent to casting it as (uint8_t*) in C. It does seem like this ought to be part of the Rust standard library, but the only similar facility is std::mem::transmute(), which does the same thing. But it also much more powerful things as well, and is therefore marked unsafe. (I’m trying to avoid having any code that needs to be marked unsafe in this project.)

For running the loaded machine code, I wrote this method:

pub fn start(&mut self, offset: u16) {
    self.running = true;
    self.rw(RPC, PC_START + offset);
    while self.running {
        let i = self.ld(self.r(RPC));
        self.rw(RPC, self.r(RPC) + 1);
        self.exec(i);
    }
}

I’ll talk more about what happens in self.exec() in the next section.

The basic execute loop

In the C code, Andrei cleverly builds a table of function pointers and indexes it with the opcode, in order to execute each instruction:

typedef void (*op_ex_f)(uint16_t instruction);
op_ex_f op_ex[NOPS] = { 
    br, add, ld, st, jsr, and, ldr, str, rti, not, ldi, sti, jmp, res, lea, trap 
};

// ...in main loop:
op_ex[OPC(i)](i);

Each function, such as add(), takes the instruction as a parameter, decodes it, and mutates the global state of the VM. In the main loop, at the point where I have self.exec(i) in my code, we have op_ex[OPC(i)](i) which decodes the opcode out of the instruction, indexes the table, and calls the function with the instruction as a parameter. A similar technique is used to execute the trap routines.

This approach of storing function pointers in an array and indexing it by opcode or trap vector is great in C, but is slightly cumbersome in Rust. You would have to do something like this in order to be equivalent to the C code:

type OpExF = fn(&mut VM, u16) -> ();

// in VM:
const OP_EX: [OpExF; NOPS] = [
    VM::br, VM::add, ..., VM::trap,
];

// ...in main loop:
OP_EX[opc(i) as usize](self, i);

Incidentally, this is why I decided above not to use an enum for the opcodes. Not only would you have to create it from a u16 when you unpack it from the instruction, you would also have to convert it to a usize in order to index the opcode table.

In Rust, a match expression is a much more natural fit:

match opc(i) {
    BR => {
        if (self.r(RCND) & fcnd(i) != 0) {
            self.rw(RPC, self.r(RPC) + poff9(i));
        }
    }
    ADD => {
        self.rw(dr(i), self.r(sr(i)) +
            if fimm(i) {
                sextimm(i)
            } else {
                self.r(sr2(i))
            });
        self.uf(dr(i));
    }
    // ...etc.
}

However, there is an even better alternative that makes the main loop much more concise, like the one in the C code! We can use the bitmatch crate to simultaneously match against bit patterns and decode parts out of them.

#[bitmatch]
fn exec(&mut self, i: u16) {
    #[bitmatch]
    match i {
        "0000fffooooooooo" /* BR */ => {
            if (self.r(RCND) & f != 0) {
                self.rw(RPC, self.r(RPC) + sext(o, 9));
            }
        }
        "0001dddsss0??aaa" /* ADD register */ => {
            self.rw(d, self.r(s) + self.r(a));
            self.uf(d);
        }
        "0001dddsss1mmmmm" /* ADD immediate */ => {
            self.rw(d, self.r(s) + sext(m, 5));
            self.uf(d);
        }
        // ...etc.
    }
}

This actually gets rid of the need for all the bit-manipulation functions that I wrote in the beginning, based on the C macros, such as opc(), fimm(), and poff9(), because bitmatch automatically does all the unpacking. The only bit-manipulation we still need to do is sign-extension when we unpack immediate values and offset values from the instructions, as we do above with sext(o, 9) and sext(m, 5).

I was curious what kind of code the bitmatch macros generate under the hood and whether it’s as performant as writing out all the bit-manipulations by hand. For that, I wrote a test program that matches against the same bit patterns as the main VM loop, but with the statements in the match arms just replaced by constants, in order to avoid cluttering the output:

#[bitmatch]
pub fn test(i: u16) -> u16 {
    #[bitmatch]
    match i {
        "0000fffooooooooo" => 0,
        "0001dddsss0??aaa" => 1,
        "0001dddsss1mmmmm" => 2,
        // ...etc.
    }
}

There is a handy utility for viewing expanded macros that you can install with cargo install cargo-expand, and then run with cargo expand --lib test (I put the test function in a dummy lib.rs file.)

Here’s what we get!

pub fn test(i: u16) -> u16 {
    match i {
        bits if bits & 0b1111000000000000 == 0b0000000000000000 => {
            let f = (bits & 0b111000000000) >> 9usize;
            let o = (bits & 0b111111111) >> 0usize;
            0
        }
        bits if bits & 0b1111000000100000 == 0b0001000000000000 => {
            let a = (bits & 0b111) >> 0usize;
            let d = (bits & 0b111000000000) >> 9usize;
            let s = (bits & 0b111000000) >> 6usize;
            1
        }
        bits if bits & 0b1111000000100000 == 0b0001000000100000 => {
            let d = (bits & 0b111000000000) >> 9usize;
            let m = (bits & 0b11111) >> 0usize;
            let s = (bits & 0b111000000) >> 6usize;
            2
        }
        // ...etc.
        _ => // ...some panicking code
    }
}

It’s looking a lot like what I’d written anyway, but with all the bit-manipulation functions inlined. The main disadvantage is that you have to AND the value with a bitmask at each arm of the match expression. But maybe that isn’t such a problem? Let’s look at the generated assembly to see what the computer actually executes. There is another Cargo tool for this, which you can install with cargo install cargo-asm and run with cargo asm --lib lc3::test4. In the result, there are actually only three AND instructions, because there are only three unique bitmasks tested among all the arms of the match expression (0b1111000000000000, 0b1111100000000000, and 0b1111000000100000). So it seems like the compiler is quite able to optimize this into something good.

First test runs

By the time I had implemented all the instructions except for TRAP, at this point I wanted to actually run a program on the LC-3 VM! Andrei has one program directly in his blog post, and another one in his GitHub repository, so those seemed easiest to start with.

Just like in the blog post, I wrote a program (in my examples/ directory so that it could be run with cargo r --example) to output the LC-3 machine code. It looked something like this:

let program: [u16; 7] = [
    0xf026,  // TRAP 0x26
    0x1220,  // ADD R1, R0, 0
    0xf026,  // TRAP 0x26
    0x1240,  // ADD R1, R1, R0
    0x1060,  // ADD R0, R1, 0
    0xf027,  // TRAP 0x27
    0xf025,  // TRAP 0x25
];
let mut file = fs::File::create(fname)?;
for inst in program {
    file.write_u16::<byteorder::NetworkEndian>(inst)?;
}
Ok(())

In order for this to work, I still needed to implement some of the TRAP routines. I had left those for last, and at that point my match expression for TRAP instructions looked like "1111????tttttttt" => self.trap(t), and my trap() method looked like this:

fn trap(&mut self, t: u8) {
    match t {
        _ => self.crash(&format!("Invalid TRAP vector {:#02x}", t)),
    }
}

For this program, we can see that three traps need to be implemented: 0x25 (HALT), 0x26 (INU16), and 0x27 (OUTU16). So I was able to add just three arms to my match expression:

0x25 => self.running = false,
0x26 => {
    let mut input = String::new();
    io::stdin().read_line(&mut input).unwrap_or(0);
    self.rw(0, input.trim().parse().unwrap_or(0));
}
0x27 => println!("{}", self.r(0)),

With this, I could run the sample program, type in two numbers, and print out their sum.

The second program sums an array of numbers. In this program, I added a TRAP 0x27 instruction right before the HALT in order to print out the answer, otherwise I couldn’t see if it was working! This also required changing R1 to R0 so that the sum is in R0 when we call the trap routine, and adjusting the offset in the LEA instruction.5

When I tried running this program, it crashed the VM! This is due to the instruction ADD R4, R4, x-1 which decrements R4 by adding -1 to it. R4 is the counter for how many array elements we have left to process, so initially it holds 10, and when we get to that instruction for the first time, we decrement it to 9. But if you look at the implementation of the ADD instruction that I wrote above, we are actually doing an unsigned addition of the lowest 5 bits of the instruction, sign-extended to 16 bits, so we are not literally decrementing it. We are adding 0xffff to 0x000a and expecting it to wrap around to 0x0009 like it does in C. But integer arithmetic doesn’t wrap in Rust!

Unless you specifically tell it to, that is. So we could use u16::wrapping_add() instead of the + operator to do the addition. But I got what I thought was a better idea, to use std::num::Wrapping! I rewrote the definition of VM at the top of the file:

type Word = Wrapping<u16>;

struct VM {
    mem: [Word; MEM_SIZE],
    reg: [Word; NREGS],
    running: bool,
}

This did require adding Wrapping() around some integer literals and adding .0 to unwrap to the bare u16 in some places, but on the whole it made the code more concise and readable. As an added bonus, this way, we are using the type system to express that the LC-3 processor does wrapping unsigned arithmetic. (I do wish that there were a nicer way to express literals of the Wrapping type though.)

And with that, the second example program works. It outputs 16, as expected. In a following post I’ll go on to explain some of the other test programs that I wrote.

Other niceties

At this point I decided to do some refactoring to make the Rust code more readable and hopefully more idiomatic as well. Inspired by the type alias for Word, I added several more, one for addresses and one for instructions, as well as a function to convert a word to an address:

type Addr = usize;
type Inst = u16;
type Reg = u16;
type Flag = Word;
type TrapVect = u8;

#[inline] fn adr(w: Word) -> Addr { w.0.into() }

Addr is convenient to alias to usize because that’s the type that we use to index the memory array. And Inst is convenient to alias to u16 because that’s what bitmatch works with.

In fact, using types in this way actually allowed me to catch two bugs in Andrei’s original C code, where the index of the register was used instead of the value contained in the register.

I also added a few convenience methods to VM for manipulating the program counter:

#[inline] fn pc(&self) -> Word { self.r(RPC) }
#[inline] fn jmp(&mut self, pc: Word) { self.rw(RPC, pc); }
#[inline] fn jrel(&mut self, offset: Word) { self.jmp(self.pc() + offset); }
#[inline] fn inc_pc(&mut self) { self.jrel(Wrapping(1)); }

With these, I could write a bunch of things to be more expressive and concise. For example, the start() method now looked like this:

pub fn start(&mut self, offset: u16) {
    self.running = true;
    self.jmp(PC_START + Wrapping(offset));
    while self.running {
        let i = self.ld(adr(self.pc())).0;
        self.inc_pc();
        self.exec(i);
    }
    Ok(())
}

I also added an iaddr() method to load an address indirectly from another address given as an offset relative to the program counter, to simplify the implementation of the LDI and STI instructions.

While I was at it, I noticed that the uf() (update flags) method always followed a store into a destination register, and I decided to rewrite it as one method dst(), which stores a value into a destination register and updates the flags register based on that value:

#[inline]
fn dst(&mut self, r: Reg, val: Word) {
    self.rw(r, val);
    self.rw(
        RCND,
        match val.0 {
            0 => FZ,
            1..=0x7fff => FP,
            0x8000..=0xffff => FN,
        },
    );
}

At this point, the VM’s main loop looked just about as concise and simple as the original C code did! The original:

static inline void br(uint16_t i)   { if (reg[RCND] & FCND(i)) { reg[RPC] += POFF9(i); } }
static inline void add(uint16_t i)  { reg[DR(i)] = reg[SR1(i)] + (FIMM(i) ? SEXTIMM(i) : reg[SR2(i)]); uf(DR(i)); }
static inline void ld(uint16_t i)   { reg[DR(i)] = mr(reg[RPC] + POFF9(i)); uf(DR(i)); }
static inline void st(uint16_t i)   { mw(reg[RPC] + POFF9(i), reg[DR(i)]); }
static inline void jsr(uint16_t i)  { reg[R7] = reg[RPC]; reg[RPC] = (FL(i)) ? reg[RPC] + POFF11(i) : reg[BR(i)]; }
static inline void and(uint16_t i)  { reg[DR(i)] = reg[SR1(i)] & (FIMM(i) ? SEXTIMM(i) : reg[SR2(i)]); uf(DR(i)); }
static inline void ldr(uint16_t i)  { reg[DR(i)] = mr(reg[SR1(i)] + POFF(i)); uf(DR(i)); }
static inline void str(uint16_t i)  { mw(reg[SR1(i)] + POFF(i), reg[DR(i)]); }
static inline void res(uint16_t i) {} // unused
static inline void not(uint16_t i)  { reg[DR(i)]=~reg[SR1(i)]; uf(DR(i)); }
static inline void ldi(uint16_t i)  { reg[DR(i)] = mr(mr(reg[RPC]+POFF9(i))); uf(DR(i)); }
static inline void sti(uint16_t i)  { mw(mr(reg[RPC] + POFF9(i)), reg[DR(i)]); }
static inline void jmp(uint16_t i)  { reg[RPC] = reg[BR(i)]; }
static inline void rti(uint16_t i) {} // unused
static inline void lea(uint16_t i)  { reg[DR(i)] =reg[RPC] + POFF9(i); uf(DR(i)); }
static inline void trap(uint16_t i) { trp_ex[TRP(i)-trp_offset](); }

My implementation:

#[bitmatch]
match i {
    "0000fffooooooooo" /* BR   */ => if (self.r(RCND).0 & f) != 0 { self.jrel(sext(o, 9)); },
    "0001dddsss0??aaa" /* ADD1 */ => self.dst(d, self.r(s) + self.r(a)),
    "0001dddsss1mmmmm" /* ADD2 */ => self.dst(d, self.r(s) + sext(m, 5)),
    "0010dddooooooooo" /* LD   */ => self.dst(d, self.ld(adr(self.pc() + sext(o, 9)))),
    "0011sssooooooooo" /* ST   */ => self.st(adr(self.pc() + sext(o, 9)), self.r(s)),
    "01000??bbb??????" /* JSRR */ => { self.rw(R7, self.pc()); self.jmp(self.r(b)); }
    "01001ooooooooooo" /* JSR  */ => { self.rw(R7, self.pc()); self.jrel(sext(o, 11)); }
    "0101dddsss0??aaa" /* AND1 */ => self.dst(d, self.r(s) & self.r(a)),
    "0101dddsss1mmmmm" /* AND2 */ => self.dst(d, self.r(s) & sext(m, 5)),
    "0110dddbbboooooo" /* LDR  */ => self.dst(d, self.ld(adr(self.r(b) + sext(o, 6)))),
    "0111sssbbboooooo" /* STR  */ => self.st(adr(self.r(b) + sext(o, 6)), self.r(s)),
    "1000????????????" /* n/a  */ => self.crash(&format!("Illegal instruction {:#04x}", i)),
    "1001dddsss??????" /* NOT  */ => self.dst(d, !self.r(s)),
    "1010dddooooooooo" /* LDI  */ => self.dst(d, self.ld(self.iaddr(o))),
    "1011sssooooooooo" /* STI  */ => self.st(self.iaddr(o), self.r(s)),
    "1100???bbb??????" /* JMP  */ => self.jmp(self.r(b)),
    "1101????????????" /* RTI  */ => self.crash("RTI not available in user mode"),
    "1110dddooooooooo" /* LEA  */ => self.dst(d, self.pc() + sext(o, 9)),
    "1111????tttttttt" /* TRAP */ => self.trap(t as u8),
}

Of course, you could legitimately complain that both are horrible soups of one- and two-letter identifiers.6 But I think in both of them, if you have the abbreviations close to hand (and you do, since the program is so small!) it’s actually easier to follow because everything fits well within one vertical screenful of text. The Rust version has the added bonus of the bitmatch patterns being very visual, and the reader not having to think about bit shifting in their head.

Here’s the key for abbreviations:

  • iInstruction
  • rRegister
  • dDestination register (“DR” in the LC-3 specification)
  • sSource register (“SR1”)
  • aAdditional source register (“SR2”)
  • bBase register (“BASER”)
  • m — iMmediate value
  • oOffset (6, 9, or 11 bits)
  • fFlags
  • tTrap vector7
  • pcProgram Counter
  • stSTore in memory
  • ldLoaD from memory
  • rwRegister Write
  • adr — convert machine word to memory ADdRess
  • jmpJuMP
  • dstDestination register STore and update flags
  • jrelJump RELative to the PC
  • sextSign EXTend
  • iaddr — load Indirect ADDRess

At this point the only thing left to do, to get the program to the equivalent level of functionality as the one in Andrei’s blog post, was to implement the rest of the trap routines.

Two of the trap routines involve waiting for a key press. This is actually surprisingly difficult in Rust, as far as I can tell, and definitely not as straightforward as the reg[R0] = getchar(); which you can do in C. You can use the libc crate, but libc::getchar() is marked unsafe.8 Instead, I ended up pulling in another dependency, the console crate, and adding a term: console::Term member to VM. With that, I could implement a getc() method that reads a character, stores its ASCII code in the R0 register, and returns the character itself:

fn getc(&mut self) -> char {
    let ch = self.term.read_char().unwrap_or('\0');
    let res = Wrapping(if ch.is_ascii() { ch as u16 & 0xff } else { 0 });
    self.rw(R0, res);
    ch
}

This by itself was enough to implement the GETC trap routine (which waits for a key press and stores its ASCII code in the lower 8 bits of R0) and the IN trap routine (which does the same thing but first prints a prompt, and echoes the character back to stdout) was not much more complicated:

0x20 => {
    self.getc();
}
0x23 => {
    print!("> ");
    let ch = self.getc();
    print!("{}", ch);
}

Next I wrote the OUT trap routine, which prints the lower 8 bits of R0 as an ASCII character. I wrote an ascii() function that converts the lower 8 bits of a machine word into a char:

#[inline]
fn ascii(val: Word) -> char {
    char::from_u32(val.0 as u32 & 0xff).unwrap_or('?')
}

// In the TRAP match expression:
0x21 => print!("{}", ascii(self.r(R0))),

Now the two remaining traps were PUTS (print a zero-terminated string starting at the address in R0) and PUTSP (same, but the string is packed two bytes per machine word). These two routines are very similar in that they both access a variable-length area of memory, starting at the address in R0 and ending with the next memory location that contains zero. I found a nice solution that feels very Rust-like to me, a strz_words() method that returns an iterator over exactly this region of memory:

fn strz_words(&self) -> impl Iterator<Item = &Word> {
    self.mem[adr(self.r(R0))..].iter().take_while(|&v| v.0 != 0)
}

The two trap routines differ in what they do with the items coming out of this iterator. For PUTS we convert each machine word to a char with ascii():

0x22 => print!("{}", self.strz_words().map(|&v| ascii(v)).collect::<String>()),

(It’s too bad that we have a borrowed value in the closure, otherwise we could just do map(ascii). On the positive side, collect::<String>() is really nice.)

PUTSP is a bit more complicated. It’s a neat trick to use flat_map() to convert our iterator over machine words into a twice-as-long iterator over bytes. However, we still have to collect the bytes into an intermediate vector so we can check if the last byte is zero, because the string might have an odd number of bytes. In that case we’d still have a final zero byte which we have to pop off the end, because the iterator doesn’t finish until we get a whole memory location that is zero.

0x24 => {
    let mut bytes = self
        .strz_words()
        .flat_map(|&v| v.0.to_ne_bytes())
        .collect::<Vec<_>>();
    if bytes[bytes.len() - 1] == 0 {
        bytes.pop();
    };
    print!("{}", String::from_utf8_lossy(&bytes));
}

Conclusion

At this point, what I had was mostly equivalent to what you have if you follow along with Andrei’s blog post until the end, so I’ll end this post here. Unlike the C version, it is not under 125 lines long, but it does clock in at just under 200 lines.9

After I had gotten this far, I spent some time improving what I had, making the VM a bit fancier and writing some tools to use with it. I intend to make this article into a series, and I’ll cover these improvements in following posts, starting with an assembler.

You can find the code in a GitHub repo. I have kept this first version apart, in an examples/first.rs file, but you can browse the other files in that repository if you want a sneak preview of some of the other things I’ll write about.

Many thanks to Federico Mena Quintero who gave some feedback on a draft of this post.


[1] In the intervening time, I didn’t write any Rust code at all

[2] “LC” stands for “Little Computer”. It’s a computer architecture described in a popular textbook, and as such shows up in many university courses on low-level programming. I hadn’t heard of it before, but after all, I didn’t study computer science in university

[3] I won’t lie, mostly by googling terms like “lc3 division” on the lazyweb

[4] This tool is actually really bad at finding functions unless they’re in particular locations that it expects, such as lib.rs, which is the real reason why I stuck the test function in a dummy lib.rs file

[5] Note that the offset is calculated relative to the following instruction. I assume this is so that branching to an offset of 0 takes you to the next instruction instead of looping back to the current one

[6] Especially since bitmatch makes you use one-letter variable names when unpacking

[7] i.e., memory address of the trap routine. I have no idea why the LC-3 specification calls it a vector, since it is in fact a scalar

[8] I’m not sure why. Federico tells me it is possibly because it doesn’t acquire a lock on stdin.

[9] Not counting comments, and provided we cheat a bit by sticking a few trivial functions onto one line with #[rustfmt::skip]

Advent of Rust 12: Typo the Ship Around

It’s once again time for another chronicle of teaching myself the Rust programming language, by doing the programming puzzles on Advent of Code 2020. That’s all I have to say by way of introductions!

Day 12, Part 1

Today’s puzzle looks a lot like Day 8, the virtual machine: here, also, we have to read a bunch of instructions in from a file, simulate executing them, and the answer is something about the state of the system. Instead of a virtual machine the system is a ship, and the instructions are directions for moving the ship: move north, south, east, or west, turn left or right, and move forward in the direction the ship is facing. The answer to the puzzle is the Manhattan distance that the ship has travelled.

I take the code from Day 8 as a starting point and build something similar:

#[derive(Debug)]
enum Direction {
    North(u8),
    South(u8),
    East(u8),
    West(u8),
    Left(u16),
    Right(u16),
    Forward(u8),
}

impl Direction {
    fn from_string(line: &str) -> Self {
        let parameter = &line[1..];
        match line.chars().next().unwrap() {
            'N' => Direction::North(parameter.parse().unwrap()),
            'S' => Direction::South(parameter.parse().unwrap()),
            'E' => Direction::East(parameter.parse().unwrap()),
            'W' => Direction::West(parameter.parse().unwrap()),
            'L' => Direction::Left(parameter.parse().unwrap()),
            'R' => Direction::Right(parameter.parse().unwrap()),
            'F' => Direction::Forward(parameter.parse().unwrap()),
            _ => panic!("Bad instruction {}", line),
        }
    }
}

struct Ship {
    latitude: i16,  // north-south distance
    longitude: i16, // east-west distance
    facing: i8,     // east = 0, increasing clockwise, degrees / 90
}

impl Ship {
    fn new() -> Self {
        Ship {
            latitude: 0,
            longitude: 0,
            facing: 0,
        }
    }

    fn go(&mut self, dir: &Direction) {
        match dir {
            Direction::North(dist) => self.latitude += *dist as i16,
            Direction::South(dist) => self.latitude -= *dist as i16,
            Direction::East(dist) => self.longitude += *dist as i16,
            Direction::West(dist) => self.longitude -= *dist as i16,
            Direction::Left(angle) => {
                self.facing -= (*angle / 90) as i8;
                self.facing += 4;
                self.facing %= 4;
            }
            Direction::Right(angle) => {
                self.facing += (*angle / 90) as i8;
                self.facing += 4;
                self.facing %= 4;
            }
            Direction::Forward(dist) => match self.facing {
                0 => self.go(&Direction::East(*dist)),
                1 => self.go(&Direction::South(*dist)),
                2 => self.go(&Direction::West(*dist)),
                3 => self.go(&Direction::North(*dist)),
                _ => panic!("Bad internal state: facing = {}", self.facing),
            },
        };
    }

    fn manhattan_distance(&self) -> i16 {
        self.latitude.abs() + self.longitude.abs()
    }
}

fn main() -> Result<(), io::Error> {
    let file = fs::File::open("input")?;
    let mut ship = Ship::new();
    read_lines(file)
        .map(|s| Direction::from_string(&s))
        .for_each(|dir| ship.go(&dir));
    println!("{}", ship.manhattan_distance());
    Ok(())
}

Some differences with Day 8’s solution are:

  • I wonder if I can give the enum a from_string method, and indeed I try it and it works.
  • I don’t have to save all the directions in a vector, because I don’t have to jump to an earlier or later instruction; I can just execute each one as soon as I read it.

This went smoothly and gave me the right answer. Aside from the usual dance of letting the compiler tell me where I forgot to borrow variables, I also forgot to put Direction:: on the enum values (too used to enums in C.) It’s also notable that I forgot, as I do in many other programming languages, that the modulo operator (%) can give you a negative result; that’s the reason why I add 4 before taking the modulo of 4.

One Rust thing that still confuses me; I’m not sure why you can get a slice of a string with &line[1..], but not get the first character with &line[0]. This is why I somewhat awkwardly use line.chars().next().unwrap() in Direction::from_string.

Day 12, Part 2

Part 2 of the puzzle reveals that each instruction is actually supposed to do something totally different. Most instructions don’t actually move the ship, they move a “waypoint” north, south, east, or west, or rotate it around the ship. Only the Forward instruction moves the ship, in multiples of the waypoint’s distance.

So I just need to write a second version of the Ship::go() method, which I’ll call move_waypoint(), that implements the new meanings for the instructions instead of the old ones. I will add additional fields to the Ship struct to keep track of the waypoint’s distance north and east of the ship, which may be negative.

To rotate the waypoint, I hoped I could do something like this:1

(self.waypoint_n, self.waypoint_e) = match (*angle / 90) {
    0 => (self.waypoint_n, self.waypoint_e),
    1 => (self.waypoint_e, -self.waypoint_n),
    2 => (-self.waypoint_n, -self.waypoint_e),
    3 => (-self.waypoint_e, self.waypoint_n),
    _ => panic!("Bad angle {}", *angle);
}

However, destructuring assignment is apparently not present yet in a released version of Rust, so this doesn’t work! I’m surprised, as pattern matching seems to be pervasive everywhere else in the language. Instead I google “rust swap variables” but then settle on two temporary variables, because let (new_waypoint_n, new_waypoint_e) = ... does work.

When running the program, I first get a panic due to integer overflow, so I change the type of latitude and longitude to i32. After fixing that, I do get an answer, but the website tells me it’s too high.

I print out each step:

println!("direction {:?} - ship ({}, {}) - waypoint ({}, {})", dir, ship.latitude, ship.longitude, ship.waypoint_n, ship.waypoint_e);

Aside from initially confusing myself about the output because I’m implementing R(n) as L(360 – n), I don’t see anything wrong with it. At this point I’m stumped; I try the example input from the puzzle description, and it gives the correct answer.

Since I had an integer overflow error before, I wonder if there was some other integer conversion error somewhere. I change all of the numeric types to be i32 everywhere; may as well, because it gets rid of the casts. But I get the same answer.

I look over my code, look over the debug output, and just can’t figure out what might be the problem! I know this is probably some typo that is sitting in a blind spot. After a long time I follow the suggestion on the “you got the wrong answer” page, and do something very uncharacteristic: read the Reddit thread. I hope that if I’m interpreting the instructions wrong, I might get a hint without reading too many spoilers. I find this comment from someone who had a typo in the West part of their code, and remarked that the example input didn’t have any West instructions, so the example still worked fine. I made almost the exact same mistake, can you spot it?

Direction::East(dist) => self.waypoint_e += *dist,
Direction::West(dist) => self.waypoint_e += *dist,

In hindsight I could have known by looking at the very first line of the debug output:

direction West(5) - ship (0, 0) - waypoint (1, 15)

The waypoint initially starts at north 1, east 10, so moving the waypoint west should make the waypoint east distance 5, not 15. Once that mistake is corrected, I get the correct answer!

Here’s the move_waypoint() function, or see the full code in the repository.

fn move_waypoint(&mut self, dir: &Direction) {
    match dir {
        Direction::North(dist) => self.waypoint_n += *dist,
        Direction::South(dist) => self.waypoint_n -= *dist,
        Direction::East(dist) => self.waypoint_e += *dist,
        Direction::West(dist) => self.waypoint_e -= *dist,
        Direction::Left(angle) => {
            let (new_waypoint_n, new_waypoint_e) = match *angle / 90 {
                0 => (self.waypoint_n, self.waypoint_e),
                1 => (self.waypoint_e, -self.waypoint_n),
                2 => (-self.waypoint_n, -self.waypoint_e),
                3 => (-self.waypoint_e, self.waypoint_n),
                _ => panic!("Bad angle {}", *angle),
            };
            self.waypoint_n = new_waypoint_n;
            self.waypoint_e = new_waypoint_e;
        }
        Direction::Right(angle) => {
            self.move_waypoint(&Direction::Left(360 - *angle));
        }
        Direction::Forward(times) => {
            self.latitude += self.waypoint_n * *times;
            self.longitude += self.waypoint_e * *times;
        }
    }
}

Afterword

Being confounded by a typo that you just can’t see is the great equalizer, it happens to everyone from time to time, no matter their level of experience … having said that, you can take steps to ensure it’s less likely to happen. For example, usually when I’m writing code, I’m verifying each piece individually against the expected results in unit tests. If I’d had a unit test for the West instruction, I’d have immediately been able to tell where the problem was. A test-first approach would have helped as well; as you can see above, once I saw the result of the faulty West instruction, it was too easy to say “oh, that looks right,” but if I’d had to write the test first, I would have had to actually think about what the result should have been.

Certainly I’m not writing unit tests here, and it’s not clear whether it’s worth it for a one-off puzzle. (I’m not even sure what unit-test frameworks are available in Rust, maybe I should find out!)

What I do find interesting is that this is the first such bug that I’ve written, during this learning exercise. More often when I have this kind of frustration, it’s because of something like dereferencing a null pointer that I thought couldn’t be null. I’m aware of the possibility that this could be wishful thinking or Rust hype, and not backed up by actual data, but I might have expected to run into more of those along the way, if writing in a language that has null pointers.


[1] In theory these are cosines and sines, but I calculated this by rotating my thumb and forefinger around in the air

Advent of Rust 8: Please Make Your Error More Error-y

It’s Day 8 of the no-longer-so-stream-of-consciousness-log of teaching myself the Rust programming language by solving the programming puzzles at Advent of Code 2020.

Today I start off by refactoring my boilerplate code some more. I got loads of friendly advice from Federico Mena Quintero including how to configure Cargo so that the main.rs doesn’t have to be in a subdirectory, some reading material on error handling which I am working my way through, and a talk which I watched last night while washing the dishes, on Rust programs as a dialogue between the programmer and the compiler. A dialogue between the programmer and the compiler is certainly what some of these puzzles have been!

Reorganizing the project structure to be flat is something I’ve wanted to do since Day 1! I create a new project with cargo new puzzle8, but then I delete the src/ directory and create puzzle8.rs in the main puzzle8 directory, and put the following in Cargo.toml:

[[bin]]
name = "puzzle8"
path = "puzzle8.rs"

(I haven’t read anything yet about TOML and am just parroting the examples in the Cargo documentation; in particular, don’t ask me what the double square brackets signify.)

I decide to start with this boilerplate in puzzle8.rs:

use std::env;
use std::fs;
use std::io::{self, BufRead};

fn main() -> Result<(), io::Error> {
    let file = fs::File::open("input")?;
    let lines: Vec<String> = read_lines(file).collect();
    println!("Hello, world! {}", lines.len());

    Ok(())
}

fn is_part2() -> bool {
    env::args().nth(1).map(|val| val == "2").unwrap_or(false)
}

fn read_lines(file: fs::File) -> impl Iterator<Item = String> {
    io::BufReader::new(file).lines().map(|res| res.unwrap())
}

Federico’s advice included moving the fs::File::open() call into the main body. I find this version of the code much shorter and easier to read.

What I’m also doing this week is enabling the Rust documentation in the DevDocs tab that I permanently have open in my browser so that I can search the API documentation more easily. This wouldn’t have been so useful to me in the first few days, since I wouldn’t have understood the documentation very well, but now it is.

Now I’m ready to get started on the puzzle!

Day 8, Part 1

Today’s puzzle involves executing some computer instructions in a made-up assembly language, and figuring out where it goes into an infinite loop. The answer to the puzzle is the value that’s in the accumulator just before the infinite loop starts.

This is exciting, we get to build a tiny virtual machine! That sounds intimidating but there are a few things that make it easy:

  • there are only 3 instruction codes (“opcodes”).
  • there is only one variable (“accumulator”).
  • there are no choices, branches, or side effects, so if you reach the same instruction for the second time, then you know you’re in an infinite loop.

I think I will start by designing a data structure and API for the virtual machine. Instructions can be a tuple of an opcode and a parameter (and looking at the input file, I see parameters greater than +128 and less than -127, but none more than three digits, so i16 seems like a good type for now). The opcodes are nop to do nothing, acc to increase or decrease the accumulator, or jmp to jump to a different instruction. I think this can be represented by an enum, if Rust has such a thing. I google “rust enum” and land on a chapter in the Rust book.

I start with this:

enum Opcode {
    NOP,
    ACC,
    JMP,
}

struct Instruction {
    opcode: Opcode,
    param: i16,
}

Reading a bit further on in the book, I find out something that is surprising to me: enums in Rust are not what is known as enums in other programming languages! They are actually variant data types. So, because the NOP instruction ignores its parameter, I can instead write the following, remembering #[derive(Debug)] from Day 2, and dispense with having a separate struct Instruction:

#[derive(Debug)]
enum Instruction {
    NOP,
    ACC(i16),
    JMP(i16),
}

Then for the virtual machine itself I will need a Vec<Instruction> for the program code, a usize for the program counter (an index pointing to which instruction in the program code is being executed), and a member for the accumulator. The puzzle description doesn’t say how many bits the accumulator is, but let’s give it a fairly large signed integer type: i32. We will also need one bit per instruction to keep track of whether the instruction has been executed before or not. We could store the program code as Vec<(Instruction, bool)>, which would waste a lot of space but be just fine for solving the puzzle. But out of interest, I also take a look at the bitvec package and it seems like it won’t be too difficult to use, so I tentatively decide to try that, and will revert to the tuple approach if I run into problems.

struct VM {
    acc: i32,
    pc: usize,
    code: Vec<Instruction>,
    visited: BitVec,
}

Now I need to decide what methods the virtual machine will have. It will need a method to convert a line of assembly language into an instruction and push the instruction into the code vector. This should probably look like assemble_line(line: &str) -> Result<(), ???>. It will also need a method to run the program until an infinite loop is detected, which I will call run() -> Result<(), ???>. I’m not sure what to put in the ??? types in both cases. In the case of assemble_line(), the possible errors could be that the string is not in the right format, or that the opcode is unknown, or the parameter is out of range. In the case of run(), the possible errors could be that an infinite loop was detected, or that the accumulator overflowed, or any number of other errors that I haven’t thought of.

I think I will need to learn how to define my own error type in Rust, so I google “rust define error type” and land on the “Defining an Error Type” page in Rust by Example and although that helpfully tells me that it’s possible to give parameters to error types, the example doesn’t actually show how to do what I want! My impression a few days ago was that Rust by Example wasn’t all that useful for me, and now that I know a bit more, I think it’s because the examples in Rust by Example tend to be too concise and basic for my purposes. Maybe they would be more useful to jog my memory if I had done these things once before.

The next search result is the Stack Overflow question “How do you define custom Error types in Rust?”. Unfortunately most of the answers to this question consist basically of “I am the author of a crate that lets you do this,” and I’ve gotten burned often enough by “Hey, you should use my npm package for this” that it seems to me I should stay far away from those! The bottom answer does usefully tell me that I don’t actually need to implement the Error trait, I can just use my own enum. It doesn’t show what that looks like, but having just read about enums I think I can figure it out from here.

enum VMError {
    InvalidString(String),
    InvalidOpcode(String),
    InvalidParameter(String),
    InfiniteLoop,
}

impl VM {
    fn assemble_line(&self, line: &str) -> Result<(), VMError> {
        Ok(())
    }

    fn run(&self) -> Result<(), VMError> {
        Ok(())
    }
}

I wonder if I have to write a constructor as well, since I don’t want to have to create a VM object by specifying values for acc, pc, code, and visited. I google “rust constructor” and don’t find a very good resource to answer my question, but I manage to piece together something from a few different resources:

fn new() -> Self {
    VM {
        acc: 0,
        pc: 0,
        code: vec![],
        visited: BitVec::new(),
    }
}

Now that I have the outline, I’d like to start by writing the code for assemble_line(). What this needs to do is take the first three characters of the string and determine the opcode from them, and parse position 4 until the end of the string into the integer parameter. Then it needs to store the resulting Instruction in the code vector, and append a zero to the visited bit vector. Here’s what I come up with:

fn assemble_line(&mut self, line: &str) -> Result<(), VMError> {
    let opcode_string = &line[..3];
    let parameter_string = &line[4..];
    let parameter = parameter_string
        .parse::<i16>()
        .map_err(|_| VMError::InvalidParameter(parameter_string.to_string()))?;
    let instruction = match opcode_string {
        "nop" => Instruction::NOP,
        "acc" => Instruction::ACC(parameter),
        "jmp" => Instruction::JMP(parameter),
        _ => return Err(VMError::InvalidOpcode(opcode_string.to_string())),
    };
    self.code.push(instruction);
    self.visited.push(false);
    Ok(())
}

A few observations:

  • self needs to be mutable, which I had forgotten.
  • It’s actually quite verbose to construct a VMError, so I wonder if I’m doing it right after all. (At first I tried simply InvalidParameter(parameter_string), but the error type needs to own the string.)
  • I don’t need VMError::InvalidString after all, it’s covered by InvalidParameter and InvalidOpcode.

I then try to write some code in main() to test the new function:

fn main() -> Result<(), io::Error> {
    let file = fs::File::open("input")?;
    let mut vm = VM::new();
    for line in read_lines(file) {
        vm.assemble_line(&line)?;
    }
    println!("{:?}", vm.code);

    Ok(())
}

I note happily that if I forget the ? on assemble_line(), the compiler will tell me that the error must be handled, even though the successful case doesn’t return anything. But other than that, I’m sad, because I get this error:

error[E0277]: `?` couldn't convert the error to `std::io::Error`
  --> puzzle8.rs:62:32
   |
58 | fn main() -> Result<(), io::Error> {
   |              --------------------- expected `std::io::Error` because of this
...
62 |         vm.assemble_line(&line)?;
   |                                ^ the trait `std::convert::From<VMError>` is not implemented for `std::io::Error`
   |
   = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
   = help: the following implementations were found:
             <std::io::Error as std::convert::From<std::ffi::NulError>>
             <std::io::Error as std::convert::From<std::io::ErrorKind>>
             <std::io::Error as std::convert::From<std::io::IntoInnerError<W>>>
   = note: required by `std::convert::From::from`

I guess this is what that Stack Overflow answer meant when it said I didn’t have to implement the Error trait if my error types lined up … and they do not line up. I try a few things (like changing the return type of main() to Result<(), impl Error>) and go back to search results for “rust custom error”, but don’t make any progress. Then I realize that there is a section in the Error Handling in Rust blog post that Federico sent me, that is relevant to this. I haven’t reached that part yet, but I skip ahead to it and read it.

Copying some code from that page, I manage to cobble this together:

impl fmt::Display for VMError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &*self {
            VMError::InvalidOpcode(opcode) => write!(f, "Unknown opcode {}", opcode),
            VMError::InvalidParameter(param) => {
                write!(f, "Parameter {} not a 16-bit integer", param)
            }
            VMError::InfiniteLoop => write!(f, "Infinite loop detected"),
        }
    }
}

impl Error for VMError {}

impl convert::From<VMError> for io::Error {
    fn from(err: VMError) -> io::Error {
        io::Error::new(io::ErrorKind::Other, err)
    }
}

The & on match &*self confuses me, here’s another lifetime <'_> that I don’t get, and on the whole this is definitely code that I wouldn’t have been able to write without copying it from somewhere, but it works! What I do understand about it is that for the ? operator to be able to convert my custom VMError into io::Error, I need to implement the From<VMerror> trait for io::Error1, and for that trait, VMError also needs to implement Error, and for Error it also needs to implement Display.

On to the run() method! This needs to clear the program counter, accumulator, and visited vector to 0, and execute the instructions one by one, updating the accumulator and program counter as necessary, and marking each one as visited. If the program counter reaches the end of the instructions, then the program is done, but if it reaches an instruction that was already visited, we have hit an infinite loop.

fn run(&mut self) -> Result<(), VMError> {
    self.pc = 0;
    self.acc = 0;
    self.visited.set_all(false);
    let code_length = self.code.len();
    while self.pc < code_length {
        if self.visited[self.pc] {
            return Err(VMError::InfiniteLoop);
        }
        self.visited.set(self.pc, true);
        match self.code[self.pc] {
            Instruction::NOP => self.pc += 1,
            Instruction::ACC(value) => {
                self.acc += value;
                self.pc += 1;
            }
            Instruction::JMP(distance) => self.pc += distance,
        };
    }
    Ok(())
}

The big mistake that I made here is that I can’t add the i16 parameter to self.acc: i32 or self.pc: usize. So I google “rust convert i16 to i32” and find out about the as operator. Writing self.acc += param as i32 works for the accumulator, but self.pc += distance as isize does not (and self.pc += distance as usize would be incorrect); but neither can I make self.pc into an isize instead of usize, because I need it to index into the vectors. What I would really like for this situation is a method that does checked addition of a signed type to an unsigned type, but I browse through the documentation for usize and such a method doesn’t seem to exist. I do find this Stack Overflow answer that has a good implementation that I can copy, though:

fn checked_jump(pc: usize, jump: i16) -> Option<usize> {
    if jump.is_negative() {
        pc.checked_sub(jump.wrapping_abs() as u16 as usize)
    } else {
        pc.checked_add(jump as usize)
    }
}

Then I can change the JMP case to self.pc = checked_jump(self.pc, distance).ok_or(VMError::InvalidJump)? once again gladly using the diagram of Rust Result/Option Transformations.

Now I test this by putting vm.run()?; in my main() function and I do indeed see that an infinite loop error gets printed. I just need to handle this error and print the accumulator, and I’ll have my answer:

let err = vm.run().unwrap_err();
match err {
    VMError::InfiniteLoop => println!("{}", vm.acc),
    _ => return Err(err.into()),
}

I’m sure this is not the most idiomatic way to expect a particular error enum, but I get my answer, and it is correct according to the Advent of Code website. The full code is a bit long so I won’t put it here, but you can scroll down to see it at the end of Part 2.

Day 8, Part 2

Part 2 of the puzzle is to figure out which instruction causes the infinite loop and should be repaired. We know that exactly one instruction has been corrupted, and that it’s either a NOP which was changed to JMP, or vice versa. (So that’s why NOP instructions have parameters!) We also now know that the program is supposed to terminate when the program counter points exactly one past the end of the code. (In Part 1 I assumed that it could point anywhere past the end, even if it was way past.)

First I refactor the Part 1 code. I add the i16 parameter to Instruction::NOP, and this requires writing the match expression as Instruction::NOP(_) =>. Then I check that we don’t jump more than 1 past the end, and return a new VMError::PastTheEnd if we do, by adding this to the end of run():

if self.pc != code_length {
    Err(VMError::PastTheEnd)
} else {
    Ok(())
}

I will have to iterate through all the instructions one by one, flip JMP to NOP and vice versa, and check if the program terminates; if not, flip back. I will add a repair_instruction(pc: &usize) method that will do the flipping:

fn repair_instruction(&mut self, pc: usize) -> bool {
    match self.code[pc] {
        Instruction::NOP(param) => {
            self.code[pc] = Instruction::JMP(param);
            true
        },
        Instruction::ACC(_) => false,
        Instruction::JMP(param) => {
            self.code[pc] = Instruction::NOP(param);
            true
        }
    }
}

I give it a return value so that I know I can skip running the program if the instruction was ACC.

Then I add the following code to main(), to flip each instruction, run the program, and flip back if that didn’t fix the problem:

for pc in 0..vm.code.len() {
    if !vm.repair_instruction(pc) {
        continue;
    }
    if vm.run().is_ok() {
        break;
    }
    vm.repair_instruction(pc);
}
println!("{}", vm.acc);

To my surprise, this all basically runs and gives me the correct answer the first time! I did give repair_instruction() a &usize parameter originally, but it turned out that wasn’t necessary. Fastest Part 2 so far!

Here’s the full code (the day-to-day boilerplate is now so small compared to the rest of the program, I may as well include it):

use bitvec::prelude::*;
use std::convert;
use std::env;
use std::error::Error;
use std::fmt;
use std::fs;
use std::io::{self, BufRead};

#[derive(Debug)]
enum Instruction {
    NOP(i16),
    ACC(i16),
    JMP(i16),
}

#[derive(Debug)]
enum VMError {
    InvalidOpcode(String),
    InvalidParameter(String),
    InvalidJump,
    PastTheEnd,
    InfiniteLoop,
}

impl fmt::Display for VMError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match &*self {
            VMError::InvalidOpcode(opcode) => write!(f, "Unknown opcode {}", opcode),
            VMError::InvalidParameter(param) => {
                write!(f, "Parameter {} not a 16-bit integer", param)
            }
            VMError::InvalidJump => write!(f, "Negative jump overflow"),
            VMError::PastTheEnd => write!(f, "Positive jump overflow"),
            VMError::InfiniteLoop => write!(f, "Infinite loop detected"),
        }
    }
}

impl Error for VMError {}

impl convert::From<VMError> for io::Error {
    fn from(err: VMError) -> io::Error {
        io::Error::new(io::ErrorKind::Other, err)
    }
}

struct VM {
    acc: i32,
    pc: usize,
    code: Vec<Instruction>,
    visited: BitVec,
}

impl VM {
    fn new() -> Self {
        VM {
            acc: 0,
            pc: 0,
            code: vec![],
            visited: BitVec::new(),
        }
    }

    fn assemble_line(&mut self, line: &str) -> Result<(), VMError> {
        let opcode_string = &line[..3];
        let parameter_string = &line[4..];
        let parameter = parameter_string
            .parse::<i16>()
            .map_err(|_| VMError::InvalidParameter(parameter_string.to_string()))?;
        let instruction = match opcode_string {
            "nop" => Instruction::NOP(parameter),
            "acc" => Instruction::ACC(parameter),
            "jmp" => Instruction::JMP(parameter),
            _ => return Err(VMError::InvalidOpcode(opcode_string.to_string())),
        };
        self.code.push(instruction);
        self.visited.push(false);
        Ok(())
    }

    fn run(&mut self) -> Result<(), VMError> {
        self.pc = 0;
        self.acc = 0;
        self.visited.set_all(false);
        let code_length = self.code.len();
        while self.pc < code_length {
            if self.visited[self.pc] {
                return Err(VMError::InfiniteLoop);
            }
            self.visited.set(self.pc, true);
            match self.code[self.pc] {
                Instruction::NOP(_) => self.pc += 1,
                Instruction::ACC(value) => {
                    self.acc += value as i32;
                    self.pc += 1;
                }
                Instruction::JMP(distance) => {
                    self.pc = checked_jump(self.pc, distance).ok_or(VMError::InvalidJump)?
                }
            };
        }
        if self.pc != code_length {
            Err(VMError::PastTheEnd)
        } else {
            Ok(())
        }
    }

    fn repair_instruction(&mut self, pc: usize) -> bool {
        match self.code[pc] {
            Instruction::NOP(param) => {
                self.code[pc] = Instruction::JMP(param);
                true
            }
            Instruction::ACC(_) => false,
            Instruction::JMP(param) => {
                self.code[pc] = Instruction::NOP(param);
                true
            }
        }
    }
}

// https://stackoverflow.com/a/54035801/172999
fn checked_jump(pc: usize, jump: i16) -> Option<usize> {
    if jump.is_negative() {
        pc.checked_sub(jump.wrapping_abs() as u16 as usize)
    } else {
        pc.checked_add(jump as usize)
    }
}

fn main() -> Result<(), io::Error> {
    let file = fs::File::open("input")?;
    let mut vm = VM::new();
    for line in read_lines(file) {
        vm.assemble_line(&line)?;
    }

    if is_part2() {
        for pc in 0..vm.code.len() {
            if !vm.repair_instruction(pc) {
                continue;
            }
            if vm.run().is_ok() {
                break;
            }
            vm.repair_instruction(pc);
        }
        println!("{}", vm.acc);
    } else {
        let err = vm.run().unwrap_err();
        match err {
            VMError::InfiniteLoop => println!("{}", vm.acc),
            _ => return Err(err.into()),
        }
    }

    Ok(())
}

fn is_part2() -> bool {
    env::args().nth(1).map(|val| val == "2").unwrap_or(false)
}

fn read_lines(file: fs::File) -> impl Iterator<Item = String> {
    io::BufReader::new(file).lines().map(|res| res.unwrap())
}

Afterword

Today’s puzzle went more smoothly than yesterday’s, especially Part 2. The most time-consuming part was figuring out all of the fuss required to make a custom error type that implements the Error trait. That was certainly not as easy as I had expected it would be, and I will probably avoid doing it for future puzzles.

In hindsight, it probably would have been a lot easier if I had used my custom error type only for runtime errors in the VM. Then I wouldn’t have had to implement the Error trait in order to be able to return those errors from main().

The bitvec package, on the other hand, was super easy to use.

Finally, to be clear, unlike in the first few days’ blog posts, I am no longer writing down every mistake that I make! I am still making plenty of mistakes, but I’m finding that it no longer helps me that much to log all of them, because I am starting to understand what the compiler errors mean. This doesn’t mean that I’ve learned quick enough to write all this code without mistakes!


[1] Is adding a trait to a standard library type as bad as monkeypatching a built-in object in JavaScript though? I’m not sure