Advent of Rust 13: Lucky Numbers

It’s time for the 13th installment of the chronicle of me doing programming puzzles from Advent of Code 2020 to teach myself the Rust programming language.

Looking at the lessons that I learned from previous days, today I resolve to be more systematic about debugging. If I get the wrong answer I will try my program on the example input first, before changing up a bunch of other things.

Day 13, Part 1

Today’s puzzle is about which bus to take from the airport. We get an input file consisting of only two lines of text: our arrival time at the airport, and a list of bus line numbers that are in service, with some xs denoting bus lines that are not in service. Each bus line departs from the airport at an interval of minutes equal to its number, so bus 77 departs when time % 77 == 0.

Since there are only two lines of text in the input file, and they both mean something different, I’m not looping over read_lines() today. To read the arrival time, I would really like to do this:

let arrival: u32 = lines.next()?.parse()?;

But, just as on Day 8, I can’t figure out how to get these dang error types to line up, without a lot of boilerplate (for example, implement From for every library error that I intend to handle, as I did and regretted on Day 8.) In my mind at least, this really ought to work:

fn main() -> Result<(), impl error::Error>

But I already tried that on Day 8 and it didn’t work, giving me errors such as:

error[E0277]: `?` couldn't convert the error to `impl std::error::Error`
 --> puzzle13.rs:7:39
  |
6 | fn main() -> Result<(), impl Error> {
  |              ---------------------- expected `impl std::error::Error` because of this
7 |     let file = fs::File::open("input")?;
  |                                       ^ the trait `From<std::io::Error>` is not implemented for `impl std::error::Error`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = note: required by `from`

error[E0277]: `?` couldn't convert the error to `impl std::error::Error`
 --> puzzle13.rs:9:36
  |
6 | fn main() -> Result<(), impl Error> {
  |              ---------------------- expected `impl std::error::Error` because of this
...
9 |     let arrival: u64 = lines.next()?.parse()?;
  |                                    ^ the trait `From<NoneError>` is not implemented for `impl std::error::Error`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = note: required by `from`
help: consider converting the `Option<T>` into a `Result<T, _>` using `Option::ok_or` or `Option::ok_or_else`
  |
9 |     let arrival: u64 = lines.next().ok_or_else(|| /* error value */)?.parse()?;
  |                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

error[E0277]: `?` couldn't convert the error to `impl std::error::Error`
 --> puzzle13.rs:9:45
  |
6 | fn main() -> Result<(), impl Error> {
  |              ---------------------- expected `impl std::error::Error` because of this
...
9 |     let arrival: u64 = lines.next()?.parse()?;
  |                                             ^ the trait `From<ParseIntError>` is not implemented for `impl std::error::Error`
  |
  = note: the question mark operation (`?`) implicitly performs a conversion on the error value using the `From` trait
  = note: required by `from`

error[E0720]: cannot resolve opaque type
  --> puzzle13.rs:6:25
   |
6  | fn main() -> Result<(), impl Error> {
   |                         ^^^^^^^^^^ recursive opaque type
7  |     let file = fs::File::open("input")?;
   |                                       - returning here with type `std::result::Result<(), impl std::error::Error>`
8  |     let mut lines = read_lines(file);
9  |     let arrival: u64 = lines.next()?.parse()?;
   |                                    -        - returning here with type `std::result::Result<(), impl std::error::Error>`
   |                                    |
   |                                    returning here with type `std::result::Result<(), impl std::error::Error>`
...
51 |     Ok(())
   |     ------ returning here with type `std::result::Result<(), impl std::error::Error>`

As far as I can tell, the first three error messages mean I’d need to implement the From trait on each of those errors, and I don’t understand the final error message.

Since I wish so badly that I could do this, I spend a bit more time searching, and find this. It’s not quite relevant to me, although I do learn some interesting facts about the Termination trait and why we can have different return types for main(). But I do see Box<Error> in some example code there, which I try instead of impl Error. The compiler tells me to use Box<dyn Error> instead. That actually works for the io::Error and ParseIntError, but I still have to unwrap() the Option from Iterator::next() — that doesn’t automatically convert into an Error.

I read about NoneError which is an experimental API, so maybe that will be possible in the future?

Anyway, I’m pleased at least that I can now use ? on Result types that carry different error types, so it’s progress, even though what I was hoping for is not (yet) possible. I guess -> Result<(), Box<dyn Error>> will become my new idiom.

Back to the puzzle! Here’s the program that I write:

use std::error::Error;

fn main() -> Result<(), Box<dyn Error>> {
    let file = fs::File::open("input")?;
    let mut lines = read_lines(file);
    let arrival: u32 = lines.next().unwrap().parse()?;

    let (bus_number, wait_time) = lines
        .next()
        .unwrap()
        .split(',')
        .filter_map(|s| s.parse::<u32>().ok()) // available bus lines
        .map(|interval| (interval, interval - arrival % interval)) // (bus_number, wait time)
        .min_by_key(|(_, wait_time)| *wait_time)
        .unwrap();

    println!("{}", bus_number * wait_time);
    Ok(())
}

I write this without any major complications, and it gives the correct answer. I learn the filter_map() and min_by_key() iterator methods along the way.

This input had so few entries that it may well have been faster to calculate it by hand, though! But then again, it’s basically a one-liner with these fantastic Iterator methods, and I didn’t even need Itertools this time.

Day 13, Part 2

The second part of the puzzle is to find a time that satisfies a number of constraints. Given the example input 17,x,13,19, you have to find a time t at which bus 17 departs, bus 13 departs 2 minutes later, and bus 19 departs 3 minutes later. (The x means there are no restrictions on what buses might depart one minute after t.) Additionally, the puzzle description gives the hint that t is at least 100000000000000, so I start by changing the integer types in my program to u64 because that won’t fit in a u32.

I first decide to try a brute force solution, because as I learned on Day 10, I might spend a lot of time dissecting the puzzle when I just could have written a program to do it without trying to be clever.

Writing my program, I’m a bit surprised about this:

error: literal out of range for `i32`
  --> puzzle13.rs:15:17
   |
15 |         let t = 100000000000000;
   |                 ^^^^^^^^^^^^^^^
   |
   = note: `#[deny(overflowing_literals)]` on by default
   = note: the literal `100000000000000` does not fit into the type `i32` whose range is `-2147483648..=2147483647`

Why can’t the compiler infer that t is used later in other expressions of type u64, or at least guess a default type for t that’s big enough to fit the literal, or at the very least suggest adding a type annotation?

Here’s the brute force program that I write:

let mut t: u64 = 100000000000000;
let mut constraints: Vec<(usize, u64)> = entries
    .enumerate()
    .filter_map(|(ix, s)| s.parse::<u64>().map(|n| (ix, n)).ok()) // (index, bus_number)
    .collect();
constraints.sort_unstable_by_key(|(_, bus_number)| *bus_number);
constraints.reverse();

loop {
    if constraints
        .iter()
        .all(|(ix, bus)| t + *ix as u64 % *bus == 0)
    {
        break;
    }
    t += 1;
}

println!("{}", t);

Many minutes later, after a coffee break, it’s still running! Clearly this will not do.

While the program is running for so long I do decide to check if it works on the example input, to carry out my resolution from the beginning of the day. In fact, the example input does not give the correct result. I am missing parentheses in (t + *ix as u64) % *bus. With the missing parentheses added, all the example inputs on the puzzle page do produce the correct answer. But given the real input, the program still runs for many minutes without finding an answer.

Taking the example input 17,x,13,19 again, we have a system where we have to determine t from the following equations, where cn are positive integers:

t = 17 × c1
t + 2 = 13 × c2
t + 3 = 19 × c3

Here we have a system with 3 equations and 4 unknowns, so we cannot solve it directly,1 because there is more than one solution. There is also the constraint that t must be the smallest possible solution that’s greater than 0 (or, greater than 100000000000000 with the real input), which fixes our solution unambiguously.

Put more generally, we have a system of equations of the form

t + bn = an × cn

where an and bn are given.

The lowest common multiple is a special case of this system where all bn = 0. Interesting that it’s so easy to calculate the lowest common multiple and not easy to calculate this!

My partner points out that all the an in the puzzle input are prime, and that there would be no answer to the puzzle if, for example, all the an were even and any of the bn were odd.

I try to speed the program up by not bothering to check numbers that I know won’t satisfy the first constraint. I set the starting number to the first number which satisfies the first constraint, skip checking the first constraint, and use a step size of the first bus line number instead of 1:

let (first_delay, first_bus) = &constraints[0];
t += (*first_bus - t % *first_bus);
t -= *first_delay as u64;
loop {
    println!("trying {}", t);
    if constraints
        .iter()
        .skip(1)
        .all(|(delay, bus)| {
            println!("  {} + {} % {} == {}", t, *delay, *bus, (t + *delay as u64) % *bus);
            (t + *delay as u64) % *bus == 0
        })
    {
        break;
    }
    t += first_bus;
}

This speeds up the calculation of the example inputs noticeably, but the calculation of the actual puzzle answer is still taking a long time.

I try running it again on the example input 17,x,13,19 and looking at the debug output to see if there are patterns. In the case of the program above, this means: the step is 19, the constraints are [(3, 19), (0, 17), (2, 13)], and the first constraint is always satisfied. I notice that the iterations that satisfy the second constraint are always 17 steps apart. This suggests to me that once I find a number that satisfies the second constraint, I can continue with a step of 19 × 17 = 323 until I find a number that satisfies the third constraint as well. In fact, this is generalizable to include the speedup that I just wrote: I can start with a step of 1, iterate until I satisfy the first constraint, multiply the step by 19 (giving 19), iterate until I satisfy the second constraint, multiply the step by 17 (giving 323), and then iterate until I satisfy the third constraint, at which point I have the answer.

After a few false starts, I manage to write the following loop:

let mut step = 1;
let mut constraints_satisfied = 0;
loop {
    match constraints
        .iter()
        .position(|(delay, bus)| (t + *delay as u64) % *bus != 0)
    {
        None => break,
        Some(ix) => {
            if ix > constraints_satisfied {
                constraints_satisfied += 1;
                step *= constraints[ix - 1].1;
            }
        }
    }
    t += step;
}

The position() method is a new thing that I’ve learned while doing this, I found it by googling “rust find index” because I was doing something like constraints.iter().enumerate().find(...) and taking the position from there. (Maybe cargo clippy should look for this pattern.)

This gives the correct output on all the example inputs, and finds the correct answer to the puzzle in a very short time. The full code is on the repository.

Afterword

Today I actually found the opposite of what I found a few days ago: writing the brute force program first was not actually that helpful, and I did have to sit down and think a lot more about the problem, and explore the input, before I was able to get the answer.

I suspect it’s still worth it to write the brute force program first when doing programming puzzles, though. It’s only not helpful in a case like this, where brute force calculation is really not feasible.


[1] It doesn’t matter that we don’t actually care about the values of cn; they are still unknowns

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

Advent of Rust 6: Please Continue

It’s that time again, time for the daily stream-of-consciousness log of me trying to teach myself the Rust programming language by doing programming puzzles at Advent of Code 2020.

In yesterday’s installment I talked about how people had been providing helpful and encouraging comments, which I appreciated! This time, I also got one unfriendly comment, complaining how I was increasing GNOME’s dependency on Microsoft with Rust. That’s just nonsense, and I don’t even understand how they got from here to there, but for avoidance of all doubt: this series is unconnected to GNOME or my volunteer work for the GNOME Foundation, and also unconnected to my employer (Igalia, not Microsoft.) It is a learning exercise for me personally.

That unpleasant formality out of the way, let’s get started!

Day 6, Part 1

Once again I start out with cargo new puzzle6 and copy over the boilerplate from yesterday.

Today’s puzzle is about customs forms: there are 26 yes-or-no questions on these forms, marked a to z. In the input file you get lines consisting of the letters for which each passenger on the plane has answered yes. The passengers are divided into groups separated by blank lines, and you need to compute for each group, how many questions were answered yes by at least one person. The answer to the puzzle is the sum of all these counts.

The line-by-line data divided into groups separated by blank lines looks a lot like Day 4, so I can reuse some code from there. Another similarity with Day 4 is that this problem seems to lend itself well to using a HashSet. If I can take the string on each line, and insert all the characters into a HashSet for each group, then I can get the per-group count by counting the number of the elements in the HashSet when we move on to the next group.

Here’s the code that I write:

let mut total = 0;
let mut current = HashSet::new();
for line in read_lines("input")? {
    if line == "" {
        total += current.len();
        current = HashSet::new();
    }
    current.extend(line.bytes());
}
total += current.len();
println!("{}", total);

This doesn’t compile because I forgot that each line is a Result, so I need to handle errors. I add two question marks:

for line in read_lines("input")? {
    if line? == "" {
        total += current.len();
        current = HashSet::new();
    }
    current.extend(line?.bytes());
}

Here I get an error that I haven’t seen yet:

error[E0382]: use of moved value: `line`
  --> src/main.rs:15:24
   |
10 |     for line in read_lines("input")? {
   |         ---- move occurs because `line` has type `std::result::Result<std::string::String, std::io::Error>`, which does not implement the `Copy` trait
11 |         if line? == "" {
   |            ----- `line` moved due to this method call
...
15 |         current.extend(line?.bytes());
   |                        ^^^^ value used here after move
   |
note: this function consumes the receiver `self` by taking ownership of it, which moves `line`

I did read about moving the other day when I was reading about ownership. As far as I can understand here, you can only use the ? operator once on a Result because it moves the value out of the Result. I try instead for line in read_lines("input")?.map(|s| s.unwrap()), and that works…

But I get a panic “No such file or directory” because I’ve forgotten to even download the input file! Once I’ve done that, I run the program again, and I get an answer, which according to the Advent of Code website, is correct!

This is a happy milestone for me. Although I can’t count on being able to write a program that compiles without errors the first time, still, I got most things right and didn’t have to change very much to make it work. I’m also noticing that I have a much better idea of where to look for things in the documentation than I did 6 days ago when I started.

Here’s the full code, without boilerplate:

fn main() -> Result<(), io::Error> {
    let mut total = 0;
    let mut current = HashSet::new();
    for line in read_lines("input")?.map(|s| s.unwrap()) {
        if line == "" {
            total += current.len();
            current = HashSet::new();
        }
        current.extend(line.bytes());
    }
    total += current.len();
    println!("{}", total);
    Ok(())
}

Day 6, Part 2

The second part of the puzzle is the same as the first, but you have to count the number of questions for which everyone in the group answered yes, instead of the questions for which anyone in the group answered yes. And once again like Day 4, this seems like a good opportunity to convert the HashSet of questions for which anyone answered yes, to a HashMap counting the number of people in the group who answered yes to each question.

I start out by reading the HashMap documentation again and notice something that I didn’t notice before: the Entry API. I’m somewhat familiar with this idiom from working on GJS, because it’s used in SpiderMonkey‘s hash maps inside the Firefox browser code. So I’m not actually all that surprised to see it in Rust, which originated as a Mozilla project.

From reading the documentation, it looks like this is how I’d increment the count of an element using the Entry API, inserting it if it wasn’t present yet:

let count = hashmap.entry(element).or_insert(0)
*count += 1;

So I figure what I can do is also keep track of the number of people in each group as I read the lines in from the file, and then when the group ends, change the HashMap into an iterator and filter on the counts that are equal to the number of people in the group — this should mean filtering down to the questions for which every person in the group answered yes.

First I want to refactor the program so that it uses the HashMap to count, but still computes the Part 1 answer. This is my attempt:

fn main() -> Result<(), io::Error> {
    let mut total = 0;
    let mut current = HashMap::new();
    for line in read_lines("input")?.map(|s| s.unwrap()) {
        if line == "" {
            total += count_answers(&current);
            current = HashMap::new();
        }
        for byte in line.bytes() {
            let count = current.entry(byte).or_insert(0);
            *count += 1;
        }
    }
    total += count_answers(&current);
    println!("{}", total);

    Ok(())
}

fn count_answers(group: &HashMap<u8, usize>) -> usize {
    group.len()
}

I’m a bit concerned about whether the compiler will be able to infer the type of the HashMap‘s values, or whether I need to explicitly specify usize, but apparently this is not a problem. It runs the first time, and I get the same answer I had before!

I refactored group.len() into count_answers() because that’s where I’m going to put the logic for Part 2. Here’s the first version that I write:

fn main() -> Result<(), io::Error> {
    let mut total = 0;
    let mut current = HashMap::new();
    let mut group_size = 0;
    for line in read_lines("input")?.map(|s| s.unwrap()) {
        if line == "" {
            total += count_answers(&current, group_size);
            current = HashMap::new();
            group_size = 0;
        }
        for byte in line.bytes() {
            let count = current.entry(byte).or_insert(0);
            *count += 1;
        }
        group_size += 1;
    }
    total += count_answers(&current, group_size);
    println!("{}", total);

    Ok(())
}

fn count_answers(group: &HashMap<u8, usize>, group_size: usize) -> usize {
    if is_part2() {
        group
            .iter()
            .filter(|(_, count): &(u8, usize)| usize == group_size)
            .count()
    } else {
        group.len()
    }
}

The first compiler error is an easily corrected mistake: I meant to write count == group_size, not usize == group_size. The second error is that once again I’ve gotten the type of a tuple in a map() or filter() function wrong: it should be &(&u8, &usize), and I still don’t understand why these pass-by-value parameters have to be borrowed. But I have learned from yesterday that I will need to dereference *count == group_size in that case, because you cannot compare &usize with usize, so I add that as well. But even now I get it wrong: I actually need to dereference it twice, **count == group_size, I guess because the tuple is borrowed as well as the elements of the tuple? This is a bit of a mystery to me.

With that, I can run the program and get an answer. The answer seems like it’s very small, and indeed when I put it into the Advent of Code website I find out that it’s wrong.

I wonder if I should start stepping through it in a debugger, but after staring at it for a while, I find that I’ve forgotten to put continue; inside the if line == "" clause! This was also a bit sloppy in Part 1, but it didn’t matter, because if the line was empty, the for byte in line.bytes() loop would also not run. But now I have group_size += 1 down below that loop, and that’s not supposed to be executed if the line was empty.

I add the continue; and get a different answer, which I put into the website and it’s correct.

Here’s the full code, minus the boilerplate:

use std::collections::HashMap;

fn main() -> Result<(), io::Error> {
    let mut total = 0;
    let mut current = HashMap::new();
    let mut group_size = 0;
    for line in read_lines("input")?.map(|s| s.unwrap()) {
        if line == "" {
            total += count_answers(&current, group_size);
            current = HashMap::new();
            group_size = 0;
            continue;
        }
        for byte in line.bytes() {
            let count = current.entry(byte).or_insert(0);
            *count += 1;
        }
        group_size += 1;
    }
    total += count_answers(&current, group_size);
    println!("{}", total);

    Ok(())
}

fn count_answers(group: &HashMap<u8, usize>, group_size: usize) -> usize {
    if is_part2() {
        group
            .iter()
            .filter(|(_, count): &(&u8, &usize)| **count == group_size)
            .count()
    } else {
        group.len()
    }
}

Afterword

Part 1 went very quickly, and Part 2 took a bit longer. This time there was a more balanced mix of Rust-beginner mistakes (that danged & operator every time!) and “normal” mistakes that I might make in another programming language (forgetting the continue statement).

I do think that I’m leaning very heavily on existing knowledge of things from other programming languages. For example, I guessed that Rust might have a continue statement without looking it up, because of all the other languages that have a continue statement. The knowledge I’m leaning most heavily on comes from C++ (for the general idea of RAII, the types that the language provides, references, and templates), and after that Python (for iterators and the idea of an extensive standard library); not so much from JavaScript or any of the other languages I’ve worked in. I might have mentioned this before, but I think progress would have been much slower had I not already been familiar with C++ and Python.

Onward to Day 7!


[0] No footnotes this time