Advent of Code 2025

Home / 2025 / 12 / 14 / #rlworkbench / #c / #aoc

Warning: This blog post contains spoilers. If you don't want any insights into this years' puzzles, stop reading now.

I couldn't resist participating in Advent of Code this year either.

Usually I challenge myself by solving the puzzles in some new kind of way. This year I decided to take a meta approach. What does that mean?

In my last newsletter, I wrote about my new project, rlworkbench, where I try to build a language workbench. What is that? What am I trying to build?

The way I think about it is that rlworkbench should be a text editor that is aware of the language of the text that is being edited. That awareness can give you syntax highlighting for example. But it can also give you more if it knows more about the language. For example, there might be an editor operation to expand the selection. If the cursor is inside a function, expanding the selection might select the whole function. Because the editor is aware of the language, it knows where the function starts and ends. Furthermore, defining the languages that rlworkbench knows about should be easy. And the norm should be to create many small domain specific languages. And when you define a language, it not only gives you editor support, but you can also define how that language should be compiled or translated.

How does that relate to Advent of Code?

Think of every day's puzzle input as its own language. The input is the program. Your job is to write a parser and code generator for that language. The parser provides editor support and the code generator compiles the puzzle input into something that can be executed. Because rlworkbench is written in C, and because I want to practice writing more C, I chose to make all code generators output C code.

So the general shape of my solutions this year looked something like this:

  1. Write a parser for day X in the meta language of rlworkbench
  2. Write a code generator that transforms the input of day X into a C program
  3. Compile and run that C program
  4. Watch it print solutions for part 1 and part 2

Since you usually don't edit the puzzle input, it is a bit contrived to see it as a language. But, hey, I wanted to experiment.

Let's walk through day 1 in a bit of detail to show my process this year. Then I will continue with some highlights from each day.

Day 1

The example input for day 1 looks like this:

L68
L30
R48
L5
R60
L55
L1
L99
R14
L82

It is a sequence of instructions for how to rotate a dial.

Here is a parser for this language, written in the meta language of rlworkbench:

main = instruction* !.;

instruction =
    | left
    | right
    ;

left[Reserved] = 'L' number '\n';

right[RuleName] = 'R' number '\n';

number = digit digit*;

digit = '0'-'9';

The meta language can describe other languages. Hence the word meta. (It is highly influenced by META II.)

The parsing begins at main. Here we say that the input is a sequence of instruction followed by the end of file. The end of file is detected by matching "not anything".

Next we say that an instruction can either be left or right (checked in that order). They are matched by checking for the character L/R followed by a number and a newline. The Reserved and RuleName are names of highlight groups that should be used for the match.

Then finally a number is one ore more digit. And a digit is any character in the range 0-9.

Now that we have a parser for this language, we can open the input file in rlworkbench and it will highlight it like this:

L68
L30
R48
L5
R60
L55
L1
L99
R14
L82

Pretty cool. The highlighting can help you gain insight into the data. For example, you could quite quickly see the distribution of left and right turns.

How can we make this run? Let's augment the parser with code generation instructions:

main = instruction*:xs !. -> {
    $dialSize
    >dialSize { "100" }
    "#include <stdio.h>\n"
    "typedef struct {\n" >
        "unsigned int positive_offset;\n"
        "unsigned int partial;\n"
        "unsigned int complete;\n"
    < "} Turn;\n"
    "static Turn turns[] = {\n" >
        xs
    < "};\n"
    "static unsigned int number_of_turns = " xs.count ";\n"
    "int main(void) {\n" >
        "unsigned int dial_position = 50;\n"
        "unsigned int times_ended_at_zero = 0;\n"
        "unsigned int times_at_zero = 0;\n"
        "unsigned int turn_index;\n"
        "unsigned int partial_counter;\n"
        "for(turn_index=0; turn_index<number_of_turns; turn_index++) {\n" >
            "for(partial_counter=0; partial_counter<turns[turn_index].partial; partial_counter++) {\n" >
                "dial_position += turns[turn_index].positive_offset;\n"
                "dial_position %= " <dialSize ";\n"
                "if (dial_position == 0) {\n" >
                    "times_at_zero += 1;\n"
                < "}\n"
            < "}\n"
            "if (dial_position == 0) {\n" >
                "times_ended_at_zero += 1;\n"
            < "}\n"
            "times_at_zero += turns[turn_index].complete;\n"
        < "}\n"
        "printf(\"Part 1: %d\\n\", times_ended_at_zero);\n"
        "printf(\"Part 2: %d\\n\", times_at_zero);\n"
        "return 0;\n"
    < "}\n"
};

instruction =
    | left:x -> { x }
    | right:x -> { x }
    ;

left[Reserved] = 'L' number:x '\n' -> {
    "{" <dialSize "-1, " x "%" <dialSize ", " x "/" <dialSize "},\n"
};

right[RuleName] = 'R' number:x '\n' -> {
    "{1, " x "%" <dialSize ", " x "/" <dialSize "},\n"
};

number = digit digit*;

digit = '0'-'9';

In the meta language, we can add semantic actions to rules with the -> { ... } syntax. It specifies what should be generated when this rule is matched. We can bind sub matches to variables using the <expression>:<name> syntax.

When we generate code for the example, we get this C program:

#include <stdio.h>
typedef struct {
    unsigned int positive_offset;
    unsigned int partial;
    unsigned int complete;
} Turn;
static Turn turns[] = {
    {100-1, 68%100, 68/100},
    {100-1, 30%100, 30/100},
    {1, 48%100, 48/100},
    {100-1, 5%100, 5/100},
    {1, 60%100, 60/100},
    {100-1, 55%100, 55/100},
    {100-1, 1%100, 1/100},
    {100-1, 99%100, 99/100},
    {1, 14%100, 14/100},
    {100-1, 82%100, 82/100},
};
static unsigned int number_of_turns = 10;
int main(void) {
    unsigned int dial_position = 50;
    unsigned int times_ended_at_zero = 0;
    unsigned int times_at_zero = 0;
    unsigned int turn_index;
    unsigned int partial_counter;
    for(turn_index=0; turn_index<number_of_turns; turn_index++) {
        for(partial_counter=0; partial_counter<turns[turn_index].partial; partial_counter++) {
            dial_position += turns[turn_index].positive_offset;
            dial_position %= 100;
            if (dial_position == 0) {
                times_at_zero += 1;
            }
        }
        if (dial_position == 0) {
            times_ended_at_zero += 1;
        }
        times_at_zero += turns[turn_index].complete;
    }
    printf("Part 1: %d\n", times_ended_at_zero);
    printf("Part 2: %d\n", times_at_zero);
    return 0;
}

So the majority of the code generation is outputting static C code. The most "dynamic" part is this:

static Turn turns[] = {
    {100-1, 68%100, 68/100},
    {100-1, 30%100, 30/100},
    {1, 48%100, 48/100},
    {100-1, 5%100, 5/100},
    {1, 60%100, 60/100},
    {100-1, 55%100, 55/100},
    {100-1, 1%100, 1/100},
    {100-1, 99%100, 99/100},
    {1, 14%100, 14/100},
    {100-1, 82%100, 82/100},
};
static unsigned int number_of_turns = 10;

The code generator transforms the input into a data structure in C that is easy to work with. We could have generated just this part and then written the rest of the C code outside the meta language. It wouldn't exactly work in this case because the dialSize is used as a variable in the meta language. But we could have generated a constant in C with this value. But for the purposes of this year's Advent of Code puzzles, I chose to write all C code in the meta language. The meta language is implemented in itself and it generates more complicated C code than this example. So writing C code in the meta language code generation part is an actual use case and if it's not easy to do, we should make it easier. After all, we are in complete control of how the meta language works.

I actually struggled to solve day 1. Not because of the new approach with meta and not because of C which is a new language for me. The main struggle I had was that I didn't want to do the inner loop at first. I thought it would be too wasteful. And then I couldn't get all arithmetic right. I actually looked up other solutions for part 2. Then I decided to just implement the inner loop and it worked fine.

From today's puzzle, I also learned that the % operator works differently in C and Python.

In C:

 7 % 5 => 2
-7 % 5 => -2

In Python:

 7 % 5 => 2
-7 % 5 => 3

Furthermore I learned that the behavior in C89 could differ between implementations if negative operators are used. That's why I wanted to use unsigned numbers in my implementation. Because those I could reason about.

Day 2

I got to practice string manipulation code in C. Like this for example. Nice!

int eq(char* one, char* two, size_t len) {
    while (len > 0) {
        if (*one != *two) {
            return 0;
        };
        one++;
        two++;
        len--;
    }
    return 1;
}

Day 3

This was probably one of the more fun days for me because I was able to solve the puzzle almost entirely in the meta language. Here is the solution for part 1:

main = bank2*:xs !. -> {
    "#include <stdio.h>\n"
    "int main(void) {\n" >
        "printf(\"Part 1: %ld\\n\", (long int)(0" xs "));\n"
        "return 0;\n"
    < "}\n"
};

bank2 = joltage2:x number* '\n' -> { " + " x };

joltage2 =
    | nine:x  joltage1:y -> { x y }
    | eight:x joltage1:y -> { x y }
    | seven:x joltage1:y -> { x y }
    | six:x   joltage1:y -> { x y }
    | five:x  joltage1:y -> { x y }
    | four:x  joltage1:y -> { x y }
    | three:x joltage1:y -> { x y }
    | two:x   joltage1:y -> { x y }
    | one:x   joltage1:y -> { x y }
    ;

joltage1 =
    | nine:x  -> { x }
    | eight:x -> { x }
    | seven:x -> { x }
    | six:x   -> { x }
    | five:x  -> { x }
    | four:x  -> { x }
    | three:x -> { x }
    | two:x   -> { x }
    | one:x   -> { x }
    ;

nine  = &'9' hit:x -> { x } | number nine:x  -> { x };
eight = &'8' hit:x -> { x } | number eight:x -> { x };
seven = &'7' hit:x -> { x } | number seven:x -> { x };
six   = &'6' hit:x -> { x } | number six:x   -> { x };
five  = &'5' hit:x -> { x } | number five:x  -> { x };
four  = &'4' hit:x -> { x } | number four:x  -> { x };
three = &'3' hit:x -> { x } | number three:x -> { x };
two   = &'2' hit:x -> { x } | number two:x   -> { x };
one   = &'1' hit:x -> { x } | number one:x   -> { x };

hit[Escape] = number;

number = '1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9';

And the cool thing is, that this also highlights the answer for each line:

987654321111111
811111111111119
234234234234278
818181911112111

The generated C code just performs the addition:

#include <stdio.h>
int main(void) {
    printf("Part 1: %ld\n", (long int)(0 + 98 + 89 + 78 + 92));
    return 0;
}

I was able to solve part 2 in the same way, but it required me to create joltage12 to joltage1 because you can not parameterize the recursion depth in meta. But here is what it looks like when run on the example:

987654321111111
811111111111119
234234234234278
818181911112111

Also note how numbers are parsed:

number = '1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9';

This was the only way to do it in the meta language before. But I added support for character ranges. So the preferred way to write this now is this:

number = '1'-'9';

It is faster and it reads better. So one idea I had with solving Advent of Code using rlworkbench was that I can improve it with feedback from real usage. Here is one example.

Day 4

This day I got to practice parsing grids i meta and practice how to represent them in C for easy access. Here is part of the parsing code:

grid = row*:xs -> {
    $rowCount
    $colCount
    "int paper_rolls[" <rowCount "][" <colCount "] = {\n" >
        xs
    < "};\n"
    "int can_be_removed[" <rowCount "][" <colCount "];\n"
    >rowCount { W[1] }
    >colCount { W[2] }
};

row = cell*:xs '\n' -> {
    #inc(W[1] 1)
    #max(W[2] xs.len)
    "{" xs "},\n"
};

And here is what it generates:

int paper_rolls[10][10] = {
    {0, 0, 1, 1, 0, 1, 1, 1, 1, 0, },
    {1, 1, 1, 0, 1, 0, 1, 0, 1, 1, },
    {1, 1, 1, 1, 1, 0, 1, 0, 1, 1, },
    {1, 0, 1, 1, 1, 1, 0, 0, 1, 0, },
    {1, 1, 0, 1, 1, 1, 1, 0, 1, 1, },
    {0, 1, 1, 1, 1, 1, 1, 1, 0, 1, },
    {0, 1, 0, 1, 0, 1, 0, 1, 1, 1, },
    {1, 0, 1, 1, 1, 0, 1, 1, 1, 1, },
    {0, 1, 1, 1, 1, 1, 1, 1, 1, 0, },
    {1, 0, 1, 0, 1, 1, 1, 0, 1, 0, },
};
int can_be_removed[10][10];

I think the meta language would benefit from some better mechanism to keep track of rows and columns. The W[x] mechanism is a hacky way to somewhat work with counters. But it is not very nice. I though about adding implicit counters for the star-operator. Maybe this is an example to revisit later to try to improve meta.

I also learned that local variables in C are not automatically initialized to 0. I got some random behavior because of it. Yey C! Furthermore it seems that the compiler can not always fully detect it.

Day 5

This puzzle included some fun with ranges. Instead of thinking of every number in a range, compute with ranges instead. I think there was a similar puzzle last year. It was satisfying to figure out.

Day 6

This puzzle was not a very good fit for meta. Or at least part 2 wasn't. Meta works well only if you can parse character by character. Not column wise. I ended up writing another meta program to rotate the input text. That is, turning this:

123 328  51 64
 45 64  387 23
  6 98  215 314
*   +   *   +

Into this:

*  1
  42
 653

+963
 842
   8

*23
 185
 571

+326
134
4

Which I could then parse for part 2. But the whole thing became very complicated.

Day 7

In this puzzle I accidentally used an integer instead of a long. The program worked fine on the example but produced garbage for the real input. Luckily I found out why quite quickly. I'm used to bignums in Python which never overflow. So I gained some more C experience the hard way.

Day 8

Brute force worked fine. Maybe thanks to the speed of C? Is there a more clever way?

I watched Advent of Code 2025 Day 8 Jonathan Paulson and learned that union find is probably an algorithm to look up for this.

I also avoided doing sorting in C here. The problem was that I needed to get the three largest things in an array. I solved it by finding the largest through linear search, and then setting that value to 0. Then I could do that three time to get the three largest things at the expense of "removing" them from the array. This was fine for this solution. But investigating more elegant solutions to this in C is something that I might want to do. There is qsort in the standard library. But what if modifying the array is not desirable? Copy the whole array and then sort? Another thing I though about was to write a function that found the largest thing that is smaller than X. In each iteration I could pass the previous value. But I would have to do something special for the initial case.

Day 9

I used the Point in polygon that I learned about last year. It shows that you get better at Advent of Code when you do more advent of code. Surprise!

I got the correct result for part 2, but took it took ~6 minutes. Can I improve?

I searched YouTube and found that coordinate compression might be something to look into.

Day 10

Today's puzzle was a little more complex to parse. So it was fun to use meta.

The actual algorithm took a long time for me to code. Partly because I struggled with arrays in C. I learned that they are copied when copying structs (not treated as pointers). More resources on this that I found useful:

I implemented a pop function by returning the first element and moving all elements in the array left:

LightsState pop_lights_state(LightsStates* fringe) {
    LightsState state;
    int i;
    assert(fringe->used > 0);
    state = fringe->states[0];
    fringe->used--;
    for (i=0; i<fringe->used; i++) {
        fringe->states[i] = fringe->states[i+1];
    }
    return state;
}

In Python these sort of functions are just available. So it was fun to practice implementing them with more primitive data types.

I didn't manage to solve part 2 of today's puzzle. When my attempt just took forever, I assumed that you had to do something more clever. Most likely involving more math and solving some kind of equation. That was not for me. Not this year at least.

Day 11

I had solved a similar problem in previous years. So I found the solution to this one quite straight forward. But once again I struggled to implement it in C. Being at this lower level is so much less convenient. But some things are actually easier here. So this was good practice.

Day 12

I was so excited about today.

I started by writing a highlighter for boards:

main = char* !.;

char =
    | hash
    | pieceOne
    | pieceTwo
    | pieceThree
    | pieceFour
    | pieceFive
    | pieceSix
    | .
    ;

hash[Meta] = '#';

pieceOne[Reserved]     = 'A' | '0';
pieceTwo[String]       = 'B' | '1';
pieceThree[CharString] = 'C' | '2';
pieceFour[Escape]      = 'D' | '3';
pieceFive[RuleName]    = 'E' | '4';
pieceSix[VariableName] = 'F' | '5';

My idea was that I would be able to more easily debug how my algorithm placed blocks on the grid. And it kind of worked. After a lot of coding on the algorithm (which again was difficult for me in C) I had something that produced states like this:

000000..12..2..2..3333333334.44.4..5....
..0..0.1122222222233.33.33.444444.55....
000000111222222222.33.33.334.44.4555....
000000.112..2..2..3333333334.44.4..5....
..0..011122222222233.33.33.444444.55....
000000.11222222222.33.33.334.44.4555....
0000001112..2..2..3333333334.44.4..5....
..0..0.1122222222233.33.33.444444.55....

So far, so good. But my search seemed to take forever. I wanted to introduce caching to see if it would help. But after ~5 hours of struggling with C, I couldn't do it. I actually switched to Python. Implemented roughly the same that I had in C and was able to add caching. I think at least. But it still took forever.

That's when I decided to quit. This was it for me this year.

I looked up solutions for day 12 on the Internet and found that searching (as I was attempting) was probably not the right way to go. Although, I found one solution that was similar to what I tried to do: ShuvraneelMitra | Advent-of-Code-2025-CPP | Day 12.

I don't have a good intuition for when a computation is going to take too long. I tried to do something clever in day 1 to avoid unnecessary work. And I tried to brute force day 12. So that's maybe something that I should practice more next year. Analyse the problem and think about if a solution would be feasible. And if not, think of other ways to attack the problem.

Summary

This year was fun.

I got to improve rlworkbench and meta with input from real world usage.

I got to practice writing more C code and learned some things the hard way.

I got ideas for algorithms to look up (maybe for next year) and new areas of C to explore.

But now I'm done. I'm glad it is over. Now I can get back to all the things I neglected during the last 12 days.


What links here?