Failing to Learn Zig via Advent of Code

January 17th, 2022

This isn't the blog post I intended to write.

In 2018 I wrote Learning Rust via Advent of Code. This was supposed to be the exciting sequel where I learn Zig. Unfortunately I failed. Zig sparked more frustration than joy and I fizzled out after 6 days.

My biggest failure was a poor decision to solve all Advent of Code puzzles in both Rust and Zig. Unfortunately I lost motivation to reimplement them in Zig. Some mild office competition didn't help. In hindsight I should have gritted my teeth and focused exclusively on Zig. Lesson learned.

I kept pretty good notes on my initial Zig experience. Rather than let it go to waste I want to share it here.

GitHub: Link

My Background

To help paint context here's a little bit about me.

I'm a game/VR developer with 15 years work experience. Most of that time has been spent in C++ and C# (Unity). I've done a mix of gameplay and systems code. I love Rust and hate Python. I don't know anything about webdev or JavaScript; and intend to keep it that way. I probably like C++ more than C, but I try to keep my C++ simple and somewhat C-like.

Zig at Handmade Seattle

A few months ago I didn't know really anything about Zig. I knew it existed, but not much else.

I attended the awesome Handmade Seattle conference where Zig had a very strong presence. Andrew Kelley, Zig's benevolent dictator of life, gave a stellar presentation on A Practical Guide to Data Oriented Design. Numerous presentations were for projects built in Zig.

My mental model is:

I don't know if that's entirely accurately. I think it's fair.

Reading about Zig

I decided to use Advent of Code to help learn Zig. I had a great time doing this with Rust back in 2018. Before AoC I did my best to read Zig docs. Here are some of the more useful pages I found:

Downloading Zig and compiling Hello World was delightfully simple by following the Getting Started page.

Advent of Code

I took a lot of notes when solving AoC puzzles. I tried to write down every question I had or problem I encountered. It's a long list.

The next few sections contain these notes in semi-raw form. I'll share my takeaways after.

Day 1

Immediately confused by anyerror!void. I think ! is Option / std::optional. So I thought ! was a prefix, but it's also a suffix? Very confused. (Note: I was wrong. ?T is Option<T> and E!T is Result<T,E>).

Can't figure out how to print an integer. Can't find documentation or an example in ZigLearn. Needed to search for std.debug.print not std.log.info to find the example. Annoying.

Building is slow. It takes about ~3 seconds minimum which is frustratingly slow when I'm fighting basic syntax errors. I wish there was a fast zig check.

Compile error messages are mediocre. They're full of useless information and compiler callstacks I don't care about.

Zig reference documentation badly needs examples. Can't figure out how to use std.fmt.parseInt.

Catching memory leaks at run-time by default is very cool! Compiler should be able to trivially detect not calling nums.deinit() at compile-time.

Can not figure out how to get the number of items in an ArrayList. The documentation does not reveal this basic information. The type of items is var. That is not helpful.

Can't figure out how to array access an ArrayList. I get a compile error of: error: array access of non-array type 'std.array_list.ArrayListAligned(u32,null)'. That sure seems like an array type to me.

Need to access myArray.items[idx] instead of myArray[idx]. I get it. But very unintuitive and requires knowledge of implementation details.

Massive callstack on failed expect is annoying.

Day 2

Can't figure out how to compare string. Can't find anything in ZigLearn or reference. Googling "zig string compare" is not helpful. This is a shockingly hard blocker.

Ah hah. There is no such thing as strings. just []const u8. Need to use std.mem.eql.

Can't figure out how to make an ArrayList of tuples. Never figured this out. Had to make a helper struct.

It's really weird that I can call myArraylist.push but need to loop over for (myArrayList.items). It feels inconsistent. I get it, but it's super unintuitive. I've used many programming languages and none behave this way.

I can't actually figure out how to run in release mode. zig build run will build and run debug. Eventually figured out that zig build -Drelease-safe builds release. The internet told me it needed to be run manually. I didn't figure out until typing this blog post I can build and run via zig build -Drelease-safe run.

std.mem.tokenize is great. Awesome feature over vanilla C when combined with foreach-like for loop.

Documentation for TypeInfo is beyond worthless.

Day 3

Should I pass the allocator to every function? Doesn't seem great. Maybe I'm supposed to create a global? Globals are evil and feel bad.

How do I print a number in binary? Googling how to do things in Zig has like a 50% chance of taking you to a GitHub issue where the feature is being discussed. This happened to me a LOT.

How do I get the length of a tokenized line? What does tokenize return? It returns anytype. Cool. Very helpful. Had to compile, fail, and see in error spew the type was []const u8.

Lack of zig-analyzer makes learning hard.

How do I make a lambda / closure / local function? There's a too complicated pattern with numerous issues. There is, of course, a GitHub issue discussing this.

const do_thing = (struct {
    fn call(self: @This(), last_number: u8, tiles: []Tile) ?usize {
        _ = self;
        // do stuff
        return num;
        }
    }
{}).call;

Bit-shifting is a monumental pain in the ass. const mask : usize = @as(usize, 1) << @truncate(u6, i);. I eventually wound up with: @truncate(u16, @as(usize, 1) << @truncate(u6, i));. Blech.

The following line doesn't compile: const mask = 1 << (num_bits - 1);. Blech.

Casting seems too complex too: @as(type, value). Blech.

Can't xor bools!? Blech.

Can't redirect output of zig build run. The following doesn't work: zig build run > c:/temp/out.txt. :(

Day 4

Parsing i32 is delightful (once I figured it out).

tokenize is delightful.

try syntax is nice and clean.

Callstack on zig compiler error is too long and not helpful. Have to scroll way up to find actual error.

zig fmt src/main.zig is nice. Wish it automatically ran on all files.

Wish there was an easy way to pretty print nested ArrayList.

Producing an array of arrays required a lot of painful trial and error.

Struggled to mutably iterate an ArrayList. The trick was: for (board.tiles.items) |*tile| { ... }. I don't get it. I found understanding value vs reference semantics in Zig very difficult and confusing.

Index checking is cool... but the error message is not helpful. thread 9888 panic: index out of bounds. What was the index? What was the slice length? This is important information.

VSCode breakpoint on exception works great.

First-class support for callstacks is stellar. Something sorely missing in C and C++.

My parser isn't working, but not sure why. Omg tokenize does not work as expected. Needed to use split not tokenize. Horrifically insidious.

Day 5

Wanted to use regular expression. There's nothing built-in. There is a GitHub project. Not sure how to use it.

No standard package manager in Zig. Gyro, zigmod, and submodules all exist. No standard.

No zig tutorial shows how to correctly import external Zig code. This feels like a huge oversight.

VSCode debugging of Zig is easy to setup.

Debugging ArrayList is not useful. Only shows first element. :(

Zig templates/generics is interesting. pub fn max(x: anytype, y: anytype) @TypeOf(x, y) { return if (x > y) x else y; }

Compile times still frustrating. I wish there was a fast zig check similar to cargo check.

Hard to cast i32*i32 to usize. (Not sure why I made this note.)

I don't actually know what @ prefix means. I think "built-in". For some unspecified definition of built-in.

Zig's inability to infer type is annoying. If I create var count and return it and the function return type is usize then var count is obviously a usize.

Day 6

No notes. Simple problem that didn't require learning anything new. Maybe I'm starting to grasp the basics?

Day 7 + Initial Failure

Did not complete in Zig.

This is where I fell off the rails. I didn't intend to stop. I said I'll catch back up in a couple of days. I never did. The motivation drained out of me for no particular reason.

The thought of picking Zig back up felt like a chore. There were things I knew I would have to learn in Zig, for example HashMap and HashSet, and the idea of fighting did not spark joy.

Upgrade to Zig 0.9 + Failure to Revisit

I tried to pick Zig + AoC back up in January after a nice holiday break. Somewhere along the lines Zig 0.9 released so I decided it would be a good exercise to upgrade.

Upgrading Zig is a manual process. Download a new zig.exe and replace your old one in the path. A little tedious.

I immediately encountered new errors about unused function parameters. My overly complex workaround for local functions stopped working, sometimes. I don't know why so I just turned it into a normal function.

Next, I had to deal with allocgate. I found the release notes for this really frustrating. The blog post explains in detail why the change was made. But it did not clearly explain what changes I needed to make to my code to make it compile. This was needlessly frustrating to figure out imho.

Once I got my code working I realized something shocking. Zig appears to not report compiler errors for functions that get optimized out. Wat? I needed to explicitly make sure all functions were called for allocgate errors to be detected. Blech.

At this point I simply have other projects that interest me more. Zig is going back on the shelf.

Thoughts on Zig

So where does this leave me? I think I have three key impressions.

  1. Zig does not currently spark joy
  2. Zig may have several profound ideas
  3. Zig is exciting and I will revisit for Advent of Code 2022

Zig is not ready for primetime. This is not controversial. The Zig team explicitly states it is not ready for production use. I concur.

I don't think Zig is ready for most programmers. It is not as far along as I thought it was.

There is very little info on Zig out there. It's very difficult to use Google to answer questions. r/zig is not active. r/adventofcode solution threads do not have Zig solutions.

The Zig Discord is very active and the #advent-of-code channel is full of very friendly, very helpful people. They answered many questions from me. Without their help I likely would have given up earlier due to frustration.

Documentation

Zig has poor documentation. The standard library reference is experimental and not useful. The language reference is pretty good, but it's a reference not a guide or tutorial.

ZigLearn is the best learning resource I found. It's a good start. It isn't the best at introducing concepts. For example it starts off with a bunch of unfamiliar test "Do Thing" function declarations without explaining what that syntax means or how to run tests. The second example introduces the try keyword, but it isn't defined. That critically important definition occurs 7 sections later, in the middle, in an off-hand comment that's trivially easy to miss.

Writing documentation is hard. I've tried to not compare Zig to Rust. However I simply have to compare Zig's lack of documentation to the absolute stellar Rust book and Rust Standard Library Documentation.

I think Zig needs to put significant resources into the "Zig Book" now. It takes years of refinement and waiting for Zig 1.0 is too late IMHO. Start now and by Zig 1.2 it should be pretty good.

Why Zig When There is Already C++, D, and Rust?

I'm going to complain briefly about the Zig page titled Why Zig When There is Already C++, D, and Rust?.

Zig is a cool language. I think it has a lot of interesting ideas. I think in the future there are numerous reasons why someone might choose to use Zig. Here are the bullet points from "Why Zig":

This list is not compelling IMHO. Zig is a cool languge with a lot of really cool ideas. This list is not cool and does include most of the features that excite me. "It's like C, but better in every way". Now that's a hell of a sales pitch.

"No hidden control flow" seems to be a rallying cry of the Zig community. That is definitely not what I would put as the #1 most compelling bullet point for a new programming language. I don't know why it's the first feature listed on every page.

I also think it's partially wrong. No one in the history of the world has ever been confused or upset by a + b calling a function. Operating overloading can be very evil. Don't allow operator. or operator-> overloads. Overloading basic math operators is fine, not confusing, and a good idea. I do a lot of 3d vector math so I'm willing to die on this hill.

fn lerp(a: Vec3f, b: Vec3f, t: f32) Vec3f {
    // Obviously good and easy to read
    return a*(1.0-t) + b*t;

    // Obviously bad and hard to read
    return add(mul(a, 1.0 - t), mul(b, t));
}

Profound Ideas

I think Zig might have several profound ideas.

No language allocator function is profound. Passing allocators into every struct function is somewhat tedious, but profound. In C++ you have the workhorse std::vector<Foo>. Specifying the allocator requires specifying a template type, which is a monumental pain in the ass. In Zig you can trivially change the allocator type. std.heap contains ArenaAllocator, FixedBufferAllocator, GeneralPurposeAllocator, and StackFallbackAllocator. Delightful.

Zig has a seemingly powerful ability to intergrate into existing projects. It's ability to cross-compile for different platforms is enviable. I think that "using Zig for isolated parts of your project" might be profound. I'm not sure. I strongly suspect integrating Zig into real projects is harder than the Zig team suggests.

Zig's comptime generic capabilities are fascinating and profound. I don't fully understand them yet. I'm not entirely sure how they compare to C++ template or Rust generics. Can Zig comptime do anything C++ templates can do? Are there limitations? I'm not sure.

Zig's error handling is profound and powerful. It's also super confusing. I failed to grok erors sets which can be merged, inferred, coerced, deferred, and traced. Error tracing is super cool and something I badly wish was in other languages. I don't love syntax such as anyerror!?u32.

Zig's colorblind async / coroutine system may be especially profound. I haven't dug deep enough to form an opinion. This could be a huge feature advantage over C and even C++.

A Bright Future

Zig did not spark my joy. I found it difficult and frustrating to learn. Lack of documentation, lack of tutorials, poor compiler errors, unintuitive types, confusing syntax, confusing reference vs value, ungoogleable, non-composable iterators, and more.

However Zig has many things I loved and expect in a modern language. Runtime safety, slices, defer, simd, tagged unions (needs syntactic sugar), foreach style for loops, comptime, a standard build system, a promise of a package manager, basic iterators, and more.

I'm excited for the future of Zig. I love that it is exploring new ideas and pushing forward the next generation of programming languages. I think it will likely be very solid when Zig 1.0 releases. That looks to be several years from now.

I've dipped my toes in the Zig water and it was chilly. I'm not ready to jump in. I will likely give Zig a more exclusive go for Advent of Code 2022. Next time I'll have a better idea of what I'm signing up for.

Thanks for reading.