For Advent of Code in 2025, I used the Zig programming language. It was fun, but overall the language is still quite young and rough around the edges. In this article, I will drone at you about the experience with no Christmas whimsy in sight.
What is Advent of Code?
Please forgive me for the obscene vanity of quoting myself from a previous year.
Each year in December leading up to Christmas, computer science enthusiasts with too much time sit at their computers solving obscure programming problems. While this is normally not special, at this time of year it’s called Advent of Code (AOC). On each of the “days of Christmas,” there is a computer science problem given, embedded in a whimsical Christmas story.
See my previous Advent Of Code article for the full explanation.
Choosing a Programming Language for AOC
This year I had a very deliberate process to choose a programming language for AOC. I wanted something that is interesting to me and a little bit challenging. After lots of hmms and ohs, I compiled a shortlist. Should I do x86 assembly again I wondered. I decided very much against assembly early on, I’ve been there and done that before. It might sound crazy, but that felt too much in my comfort zone. I wanted something with a feeling of familiarity, but not something I feel no real challenge in. Zig naturally bubbled to the top when my friend expressed some interest. I have no regrets, I had a wonderful time yapping with them about what I learned. Up until this year, my experience with Zig was super low; I knew of it from friends and have touched it once or twice over the years. Never before have I dived deep. I barely had a handle on the basic syntax.
Admittedly, Zig is not a very good fit – Advent of Code is primarily about solving concise self-contained puzzles. It is no secret that low level programming languages are not the fastest to write and debug – Zig is one such language. Furthermore, as you’ll discover with me, Zig is very boilerplate-heavy. There are no fancy one-liners with zig, you have to keep things very explicit. But it’s fun! I wouldn’t have it any other way.
Practicing for AOC
For the late days of November, I spent much of my time practicing some Zig. I didn’t want to jump in with zero knowledge on the first day, that is a sure way to fail. I started with ziglings, they’re game-ified exercises for practicing the language syntax. I didn’t stick with it for long, just enough until I felt comfortable enough to start on some tiny projects. I am a project-based learner at heart, and I lean into that wherever possible.
The first projects I did to prepare were familiar advent of code problems from last year. When the basic syntax came naturally to my fingers, I moved on to another project of sorts. The next project was making some pre-written code. Nothing too complicated, it was just helpers for simple things like read this file into memory and other things that were common and surprisingly tedious in zig.
I did admittedly create a hashbag data-structure, but I did not end up using it in this year’s problems. That may been better to leave out. It was added to later, but only after the 2025 problems started. Next year, If I do use my own library, I definitely want it to be much more minimal. There is a threshold in my mind where too much prepared code feels like cheating. This year I got uncomfortably close to the line.
One thing that is quite apparent about Zig is its boilerplate. You have to be very explicit and that gives you lots of control, but it also introduces problems of affluence. Working out how to hammer the build scripts into place was quite an ordeal. Zig now has its own self-contained build system. The build.zig file has become fully fledged and required everywhere in the Zig ecosystem. It’s also not well documented yet, missing things is quite common of the youthful language. In order to add a dependency, you need to correctly identify the package, identify its root module, and import it. All this does is include it in your build process and make it available to import. In all, a basic build script for hello world is around 50 lines of boilerplate. To its credit, I didn’t have to write all of it, much of it was generated by the tooling. But that I did have to write was grueling.
Regex in Zig
Zigex? No, that sounds weird. There are very particular kinds of patterns and issues that AOC problems expose. The most obvious one is that every problem starts with text, so you must perform some string manipulation, there is no way around it. In many cases heuristics are enough, other times a regex is convenient, and sometimes you may procedurally parse each line.
Zig can do all of these things, as a general purpose language should. As you may guess, it isn’t as easy as it could be. The clearest hurdle is the lack of regex in the zig standard library. There is simply no built-in way to handle regular expressions in zig. There are workarounds sure, but they all take some consideration and decision making that most modern programming languages never ask of you.
I ended up settling on MVZR by mnemnion. It is a humble but easy to use and reason about. What I particularly like about it is that it’s written fully in zig, the code is approachable, and makes no assumptions about allocators. It’s all very much in the spirit of zig. I would like to get more opportunity to use it, I only ended up making good use of it during my preparation week. Another alternative I looked into was zig-regex by tiehuis. It looks good, but it is too elaborate for my liking.
The emergency backup option was to use libc’s posix regex. While it is feature rich and I understand how to use it, it suffers from many issues. It does not have good cross-platform support. It does hidden allocations too which is not idiomatic in zig. Most vitally, unnecessary C bindings didn’t feel in the spirit of this challenge. Advent of code is supposed to be a fun challenge after all, not an exercise in retreading walked paths.
Memory Management in Zig
Thankfully, zig has some functions of splitting strings. If you have any experience with C, you may find this a huge relief like I do. They do require some thought however. The string manipulation functions are actually generic functions which return slices which you must manage correctly. The generics system is used very efficiently here. (The generics system is impressive but unfortunately isn’t that relevant to this discussion so let us both agree it’s very neat indeed and move swiftly on.)
The managed memory part of zig is what causes the most inconvenience with strings. You must always make your allocations clear, even if you never intend to do any cleanup. At the start of every program, you always need to choose an allocator. For most of the problems, I just went with this because it was easy familiar. The Debug allocator is great if you’re new. It acts much like C’s malloc and free.
// create the beginner friendly allocator
var debug_alloc = std.heap.DebugAllocator(.{}){};
const alloc = debug_alloc.allocator();
defer _ = debug_alloc.deinit();
// use the allocator for a dynamic array
var list = std.ArrayList(i32).empty;
// free the list at the end of scope
defer list.deinit(alloc);
try list.append(alloc, 1);
As I got more comfortable, I experimented more with zig’s unique allocators paradigm. A fun party trick I could get away with is to run an entire program off the stack!
// buffer allocator
const kilobyte = 1024;
var allocator_backing = [1]u8{0} ** (100 * kilobyte);
var buffer_alloc = std.heap.FixedBufferAllocator.init(allocator_backing[0..]);
const balloc = buffer_alloc.allocator();
This FixedBufferAllocator is great. It begs the question of what it even means for something to be ‘on the heap’. It blows the conventional definitions out of the water. This ‘heap’ was stored fully on the stack with minimal program modifications. This is what zig does best, maximum low-level modularity and control.
Using an arena here instead of a general purpose allocator does make the meat of the code much easier to manage. Else, you will have blocks of code like this peppered all over your main method. Invaluable if you need fine memory control over long-running programs but useless for a tiny program that runs for a second.
// This is clutter!
defer {
for (lines.items) |line| {
alloc.free(line);
}
lines.deinit(alloc);
}
This allocator configuration means you never have to bother freeing anythng*!
// A solution to clutter
var debug_alloc = std.heap.DebugAllocator(.{}){};
defer _ = debug_alloc.deinit();
var arena = std.heap.ArenaAllocator.init(debug_alloc.allocator());
// will free all memory at end of scope
defer _ = arena.deinit();
const alloc = arena.allocator();
Once again, zig is full on with its power in this niche. This naturally brings up the topic of nested allocators. Here I am using an arena allocator backed by a general purpose allocator for convenience. The general purpose allocator does some assertions to prove that I am correctly handling the memory. This could easily be swapped out with any other alocator like above. For maximum speed, I may use the PageAllocator. If I can be sure it’s small I may use the FixedBufferAllocator and so on.
This ability to compose allocators brings up many opportunities for super efficient memory usage. I expect it will serve larger programs quite well. There are particular patterns which may be best served by creating small short-lived allocators to solve a very particular problem. For example, if you have a function that processes data into various hashmaps of all one data type for analysis, you may chose to back that with an arena, and the memory pool may be backed by the general purpose allocator. I would much like to explore this further.
Other Roadblocks
Zig is impressive but not mature enough to rely on yet. There are random things that waste your time like the Vscode LSP crashing because the latest binary wasn’t published. The build system quite frankly is hell with so little documentation. The documentation issues plague this language. Anything that is documented well, is most likely out of date and irrelevant. It’s the missing societal infrastructure and unfinished features that shows. I even had one issue I submitted get lost because the Zig team has migrated to a brand new platform. I only found this out a month later, for a while I was confused that I wasn’t allowed to respond on the ticket. I now hold the sole privilege of being the last open issue on the abandoned Zig github repo.
Of particular importance, is the poor integration with debuggers. I crow-barred LLDB support back into the compiler by telling to revert to the LLVM backend. This made compilation slower to boot.
const exe = b.addExecutable(.{
// allows for llvm debugger to work correctly
.use_llvm = true,
// hidden fields ...
});
I had no interest in the alternative of tracking down a compatible debugger from cryptic outdated Reddit comments.
I’m sure if I were doing anything bigger picture than AOC, I would have hit much more substantial roadblocks. Regardless, these were plenty enough to completely throw off a beginner.
Should You use Zig?
Yes, you definitely should. Not for anything production ready, but to explore this new way of thinking in your own time. All too often, I see programmers stuck in a rut, only using the language that comes easiest to them, always solving problems in the same way. The truth is, true mastery of a skill requires stepping out of your comfort zone and embracing lots of different approaches. The most obvious solution may solve the problem sure, but what does anyone learn from that? Nothing.
Innovation takes time, you should spare some of yours for the Zig Software Foundation.