Three days of Advent of Code in Hurl

Monday, December 18, 2023

Every year I do some of Advent of Code. One year I completed it, but usually I just do some of it as a social thing with friends and then taper off as interest wanes. This year, I did three days of it, and stopped because I really truly did not want to write more solutions in the language I chose.

See, previous years I made a reasonable choice, like Rust. But this year, since I wrote a programming language, I decided to do at least three days of Advent of Code in it, and more if I wanted to. (Dear reader, I did not want to.)

These three days of it were very useful in getting comfortable with Hurl and they were also critical in developing Hurl's built-ins and standard library to a reasonable point. I'm pretty confident now that you could do all of Advent of Code in it. And I'm also now free from Hurl, and so are you: this is the last either of us need to think about it1.

If you want to see all the solutions I've implemented, they're in Hurl's repo as examples and tests2. For now, I'll walk through one solution and how it works.

Deep dive of day 1, part 2

Day 1 part 2 had some interesting flair to it where it is short but also interesting enough. The premise is that you have a document where each line contains characters and you're trying to find the numbers on each line. You take the first and last single-digit number, then concatenate them together, then sum all such numbers from all the lines. But they also might be written as English words.

Here's a short example of a document.


From the first line we get "one" and "4", so that becomes 14, and the second line gives us "7" and "six", which becomes 76. The solution for this document then is 14 + 76 = 90.

The first thing we do is import the standard library functionality we will need. The paths here are relative to the source file you run, so they may change for different users.

include "../lib/loop.hurl";
include "../lib/if.hurl";

Then we can read in our input. We use built-ins for reading the file's input and breaking it into lines. Breaking it into lines could have been done in Hurl, but I made it a built-in to save time.

let input = read_file("./aoc/input/day1.txt");
let lines = str_lines(input);

Now we can iterate over the lines and extract the solution, once we define the extract_nums function.

let total = 0;

for_each(lines, func(line) {
    try {
    } catch as val {
        total = total + val;

println("solution: ", total);

The for_each here is defined in Hurl's standard library, only using Hurl itself (exceptions and recursion). It accepts a list or string as its first argument and a function to call on each element as its second argument. Inside of there, we use a try-catch to get the value of each line via the extract_nums function and add it into the running total, which we'll then print out.

Now let's define extact_nums. We know basically what it needs to do: find the first and last single-digit number on each line. Some may be a literal digit, others may be an English word representing that digit.

Here's the basic structure. We break the string into its individual characters, then we find the first and last numbers. We convert the numbers to strings, concatenate them together, then cast them back to a number. Finally we can hurl the result to our caller.

let extract_nums = func(line) {
    let chars = str_chars(line);

    let first = 0;
    # TODO: find the first number

    let last = 0;
    # TODO: find the last number

    let first = "" + first;
    let last = "" + last;

    hurl as_num(first + last);

Finding the first and last number are essentially the same, just one starts from the end, so I'll skip one here. To find the first number, we use until (defined in Hurl in the standard library) to iterate through the list until we find a number, then we break. The index is tracked in an element of a list, passed in as [0] here to start iteration at the beginning.

For each element of the list, our condition for stopping is "is this a number?" and if so, we save it and halt iteration. Otherwise we run the loop body, which increments our index for the next iteration.

try {
    until(func(locals) {
        try {
            is_number(line, locals.1);
            # TODO: define is_number
        } catch as result {
            if(func() {
                hurl result.1;
            }, func() {
                first = result.2;

            hurl result.1;
    }, func(locals) {
        hurl [locals.1 + 1];
    }, [0]);
} catch as val {

And that just leaves the last bit, which is defining our is_number function. This one leverages the if_else also defined in the standard library, and also takes advantage of Hurl's lists being 1-indexed. We keep track of the result as a list of [boolean, int] to indicate whether it is a number and, if so, what the number is.

The first thing we do is check the condition for whether the current index points to a digit. If so, we go to the true case, and we set the result to the character at that position cast to a number. Otherwise, we loop through a list of the first 9 numbers written as English words, and check whether or not they're contained as a substring starting at our index. If so, we set the result.

And at the end, we just hurl what we found!

let is_number = func(line, index) {
    let result = [false, 0];
    if_else(func() {
        hurl is_digit(at(line, index));
    }, func() {
        result = [true, as_num(at(line, index))];
    }, func() {
        try {
            for(9, func(locals) {
                let target = at(word_numbers, locals.1);
                let slice = slice(line, index, index + len(target));
                if(func() {
                    hurl slice == target;
                }, func() {
                    result = [true, locals.1];

                hurl [];
            }, []);
        } catch as default {

    hurl result;

Then it can all be put together into one listing, and we run it and it works! It gets the right answer and takes a good long time to compute it, but it does get there eventually.

You can see the full listing in the repo. I've changed the order of things here and skipped a few pieces here to present it more easily, but otherwise it's the same code.

Plans for next Advent of Code

So, this was fun. Honestly, it was enjoyable to do a few days of Advent of Code in this, uh, treat of a language. (It was my penance.) But I only really wanted to do it for a few days. Next year, what will I do?

There are a few options I'm considering for next year:

  • Assembly: this seems like a fun challenge for a few days, although probably fairly mind bending! That could be a benefit.
  • Languages made by other Recurse Center people: this could be a fun way to storm through multiple languages, like one of my RC friends did one year.
  • Next year's language: I intend to make another language next year (something more normal, to focus on some of the "make it nice to use" aspects). If I follow through, I'll have to use it at least some!
  • Something... normal? I could always use Python or Rust and do some of it the normal way! It's fun to go through it with friends, so I could go back to doing it socially next year!

Advent of Code is a lot of fun, and it's a nice way to get exposure to different ideas and new languages and new technologies, so I'm excited to see what penance I bring on myself for the next one!


Well, I do intend to submit something about Hurl to SIGBOVIK so if that gets accepted, people will hear about it again.


We are far enough into Advent of Code that posting the solutions is fine, I think. Also props to anyone who actually gets this running or translates the Hurl code into something else, so go for it.

If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts and support my work, subscribe to the newsletter. There is also an RSS feed.

Want to become a better programmer? Join the Recurse Center!
Want to hire great programmers? Hire via Recurse Center!