Skip to main content

Advent of Code '22 in Rust

· 9 min read
Matej Focko

Let's talk about the preparations for this year's Advent of Code.

Choosing a language

When choosing a language for AoC, you usually want a language that gives you a quick feedback which allows you to iterate quickly to the solution of the puzzle. One of the most common choices is Python, many people also use JavaScript or Ruby.

Given the competitive nature of the AoC and popularity among competitive programming, C++ might be also a very good choice. Only if you are familiar with it, I guess…

If you want a challenge, you might also choose to rotate the languages each day. Though I prefer to use only one language.

For this year I have been deciding between Rust, C++ and Pascal or Ada.

I have tried Rust last year and have survived with it for 3 days and then gave up and switched to Kotlin, which was pretty good given it is “Java undercover”. I pretty much like the ideas behind Rust, I am not sure about the whole cult and implementation of those ideas though. After some years with C/C++, I would say that Rust feels too safe for my taste and tries to “punish me” even for the most trivial things.

C++ is a very robust, but also comes with a wide variety of options providing you the ability to shoot yourself in the leg. I have tried to solve few days of previous Advent of Code events, it was relatively easy to solve the problems in C++, given that I do not admit writing my own iterator for enumerate

Pascal or Ada were meme choices :) Ada is heavily inspired by Pascal and has a pretty nice standard library that offers enough to be able to quickly solve some problems in it. However the toolkit is questionable :/

Choosing libraries

Preparations for Rust

All of the sources, later on including solutions, can be found at my GitLab.

Toolkit

Since we are using Rust, we are going to use a Cargo and more than likely VSCode with rust-analyzer. Because of my choice of libraries, we will also introduce a .envrc file that can be used by direnv, which allows you to set specific environment variables when you enter a directory. In our case, we will use

# to show nice backtrace when using the color-eyre
export RUST_BACKTRACE=1

# to catch logs generated by tracing
export RUST_LOG=trace

And for the one of the most obnoxious things ever, we will use a script to download the inputs instead of “clicking, opening and copying to a file1. There is no need to be fancy, so we will adjust Python script by Martin2.

#!/usr/bin/env python3

import datetime
import yaml
import requests
import sys


def load_config():
with open("env.yaml", "r") as f:
js = yaml.load(f, Loader=yaml.Loader)
return js["session"], js["year"]


def get_input(session, year, day):
return requests.get(
f"https://adventofcode.com/{year}/day/{day}/input",
cookies={"session": session},
headers={
"User-Agent": "{repo} by {mail}".format(
repo="gitlab.com/mfocko/advent-of-code-2022",
mail="me@mfocko.xyz",
)
},
).content.decode("utf-8")


def main():
day = datetime.datetime.now().day
if len(sys.argv) == 2:
day = sys.argv[1]

session, year = load_config()
problem_input = get_input(session, year, day)

with open(f"./inputs/day{day:>02}.txt", "w") as f:
f.write(problem_input)


if __name__ == "__main__":
main()

If the script is called without any arguments, it will deduce the day from the system, so we do not need to change the day every morning. It also requires a configuration file:

# env.yaml
session: ‹your session cookie›
year: 2022

Libraries

Looking at the list of the libraries, I have chosen “a lot” of them. Let's walk through each of them.

tracing and tracing-subscriber are the crates that can be used for tracing and logging of your Rust programs, there are also other crates that can help you with providing backtrace to the Sentry in case you have deployed your application somewhere and you want to watch over it. In our use case we will just utilize the macros for debugging in the terminal.

thiserror, anyhow and color-eyre are used for error reporting. thiserror is a very good choice for libraries, cause it extends the Error from the std and allows you to create more convenient error types. Next is anyhow which kinda builds on top of the thiserror and provides you with simpler error handling in binaries3. And finally we have color-eyre which, as I found out later, is a colorful (wink wink) extension of eyre which is fork of anyhow while supporting customized reports.

In the end I have decided to remove thiserror and anyhow, since first one is suitable for libraries and the latter was basically fully replaced by {color-,}eyre.

regex and lazy_static are a very good and also, I hope, self-explanatory combination. lazy_static allows you to have static variables that must be initialized during runtime.

itertools provides some nice extensions to the iterators from the std.

My own “library”

When creating the crate for this year's Advent of Code, I have chosen a library type. Even though standard library is huge, some things might not be included and also we can follow KISS. I have 2 modules that my “library” exports, one for parsing and one for 2D vector (that gets used quite often during Advent of Code).

Key part is, of course, processing the input and my library exports following functions that get used a lot:

/// Reads file to the string.
pub fn file_to_string<P: AsRef<Path>>(pathname: P) -> String;

/// Reads file and returns it as a vector of characters.
pub fn file_to_chars<P: AsRef<Path>>(pathname: P) -> Vec<char>;

/// Reads file and returns a vector of parsed structures. Expects each structure
/// on its own line in the file. And `T` needs to implement `FromStr` trait.
pub fn file_to_structs<P: AsRef<Path>, T: FromStr>(pathname: P) -> Vec<T>
where
<T as FromStr>::Err: Debug;

/// Converts iterator over strings to a vector of parsed structures. `T` needs
/// to implement `FromStr` trait and its error must derive `Debug` trait.
pub fn strings_to_structs<T: FromStr, U>(
iter: impl Iterator<Item = U>
) -> Vec<T>
where
<T as std::str::FromStr>::Err: std::fmt::Debug,
U: Deref<Target = str>;

/// Reads file and returns it as a vector of its lines.
pub fn file_to_lines<P: AsRef<Path>>(pathname: P) -> Vec<String>;

As for the vector, I went with a rather simple implementation that allows only addition of the vectors for now and accessing the elements via functions x() and y(). Also the vector is generic, so we can use it with any numeric type we need.

Skeleton

We can also prepare a template to quickly bootstrap each of the days. We know that each puzzle has 2 parts, which means that we can start with 2 functions that will solve them.

fn part1(input: &Input) -> Output {
todo!()
}

fn part2(input: &Input) -> Output {
todo!()
}

Both functions take reference to the input and return some output (in majority of puzzles, it is the same type). todo!() can be used as a nice placeholder, it also causes a panic when reached and we could also provide some string with an explanation, e.g. todo!("part 1"). We have not given functions a specific type and to avoid as much copy-paste as possible, we will introduce type aliases.

type Input = String;
type Output = i32;
tip

This allows us to quickly adjust the types only in one place without the need to do regex-replace or replace them manually.

For each day we get a personalized input that is provided as a text file. Almost all the time, we would like to get some structured type out of that input, and therefore it makes sense to introduce a new function that will provide the parsing of the input.

fn parse_input(path: &str) -> Input {
todo!()
}

This “parser” will take a path to the file, just in case we would like to run the sample instead of input.

OK, so now we can write a main function that will take all of the pieces and run them.

fn main() {
let input = parse_input("inputs/dayXX.txt");

println!("Part 1: {}", part_1(&input));
println!("Part 2: {}", part_2(&input));
}

This would definitely do :) But we have installed a few libraries and we want to use them. In this part we are going to utilize tracing (for tracing, duh…) and color-eyre (for better error reporting, e.g. from parsing).

fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_target(false)
.with_file(true)
.with_line_number(true)
.without_time()
.compact()
.init();
color_eyre::install()?;

let input = parse_input("inputs/dayXX.txt");

info!("Part 1: {}", part_1(&input));
info!("Part 2: {}", part_2(&input));

Ok(())
}

The first statement will set up tracing and configure it to print out the logs to terminal, based on the environment variable. We also change the formatting a bit, since we do not need all the fancy features of the logger. Pure initialization would get us logs like this:

2022-12-11T19:53:19.975343Z  INFO day01: Part 1: 0

However after running that command, we will get the following:

 INFO src/bin/day01.rs:35: Part 1: 0

And the color_eyre::install()? is quite straightforward. We just initialize the error reporting by color eyre.

caution

Notice that we had to add Ok(()) to the end of the function and adjust the return type of the main to Result<()>. It is caused by the color eyre that can be installed only once and therefore it can fail, that is how we got the ? at the end of the ::install which unwraps the »result« of the installation.

Overall we will get to a template like this:

use aoc_2022::*;

use color_eyre::eyre::Result;
use tracing::info;
use tracing_subscriber::EnvFilter;

type Input = String;
type Output = i32;

fn parse_input(path: &str) -> Input {
todo!()
}

fn part1(input: &Input) -> Output {
todo!()
}

fn part2(input: &Input) -> Output {
todo!()
}

fn main() -> Result<()> {
tracing_subscriber::fmt()
.with_env_filter(EnvFilter::from_default_env())
.with_target(false)
.with_file(true)
.with_line_number(true)
.without_time()
.compact()
.init();
color_eyre::install()?;

let input = parse_input("inputs/dayXX.txt");

info!("Part 1: {}", part_1(&input));
info!("Part 2: {}", part_2(&input));

Ok(())
}

Footnotes

  1. Copy-pasting might be a relaxing thing to do, but you can also discover nasty stuff about your PC. See this Reddit post and the comment.

  2. GitHub profile

  3. Even though you can use it even for libraries, but handling errors from libraries using anyhow is nasty… You will be the stinky one ;)