DEV Community

Cover image for Zig (notes)
Daniel Fitzpatrick
Daniel Fitzpatrick

Posted on

Zig (notes)

NOTE: The numbered bullet points are written correctly in the markdown, but dev.to's markdown renderer overwrites them. Sorry about the inconvenience.

This is a strange entry.

I was fascinated by Andrew Kelley’s interview on the Corecursive podcast and wanted to learn more about Zig. There isn’t much material (yet) because it’s a young project, and short of reading the beefy language doc, I decided to go with the primer at ziglearn.org.

This blog post isn’t well organized or edited - it is a humble dump of notes, thoughts, and questions I had while reading the material. It is (mostly) divided into chapters that follow the source material.

This part is important. Andrew stated in the interview that Rust users like to attack Zig. I write Clojure and have no idea what the politics in either camp are like. Many of the bullet points in these notes are geared toward discovering where the footguns are hidden, but that shouldn’t be mistaken as an agenda or part of some campaign to throw shade at Zig.

I am honest and vulnerable with what are likely many stupid questions and conclusions.

Basics

  1. Are type assignment bugs discovered at runtime or compile time? I’m especially interested in branching expressions in the rvalue of type assignments.
  2. Where does allocation take place? Some general guidelines would be helpful for reasoning about some code.
  3. Why does this code need a return statement?
test "returning an error" {
    failingFunction() catch |err| {
        try expect(err == error.Oops);
        return;
    };
}
Enter fullscreen mode Exit fullscreen mode
  1. Is there some way to query this?

Error unions returned from a function can have their error sets inferred by not having an explicit error set. This inferred error set contains all possible errors which the function may return.

fn createFile() !void {
    return error.AccessDenied;
}
Enter fullscreen mode Exit fullscreen mode
  1. Are ignored error unions caught at compile time or runtime?
  2. Why can’t cases fall through to other branches? Philosophical or technical reasoning?
  3. Any plans for something like D’s immutable type qualifier? While occasionally useful, const is too nuanced to not get wrong once in a while.

Type Qualifiers

  1. Exhaustive switch - does it also check for exclusivity?
  2. For the above, do these checks happen at compile time?

NOTE TO SELF: functions which take or return arrays, slices, slices known at compile time, and pointers may have different semantics. Pass by value/reference may muddy the waters even further. Need to investigate.

I am sensing a pattern that pointers are going to be behind most footguns. Are pointers prevalent or idiomatic in zig (outside of c interop?)

  1. Interesting how enum member functions are both static and virtual. I can’t imagine this ever being useful but I can imagine wasting time to look this up when debugging.
  2. I’m guessing that Union types and structs can’t be used inside of arrays unless it’s an array of pointers. Union types can be used inside of structs though.
  3. Tagged unions essentially give us Rust-style enums. Cool.
  4. Can I get a pointer to fn parameter and modify the parameters that way? Hopefully not…
  5. How does the conditional work in the following code if there are no truthy values? eventuallyNullSequence() returns ?u32.
test "while null capture" {
    var sum: u32 = 0;
    while (eventuallyNullSequence()) |value| {
        sum += value;
    }
    try expect(sum == 6); // 3 + 2 + 1
}
Enter fullscreen mode Exit fullscreen mode
  1. If optionals don’t take extra memory and they use a pointer to memory address 0 then I’m guessing optionals are always heap allocated?
  2. Are labels common in idiomatic zig? I find them difficult to read.
  3. This is super cool and useful in conjunction with Union types and numeric widening const b: if (a < 10) f32 else i32 = 5; Can it be used even if the condition is unknown at compile time?
  4. Something about this feels too much like the C preprocessor and “what color is your function”. Am I okay with it? Unsure.
fn Matrix(
    comptime T: type,
    comptime width: comptime_int,
    comptime height: comptime_int,
) type {
    return [height][width]T;
}
Enter fullscreen mode Exit fullscreen mode
  1. The amount of reflection, even at compile time, is very cool.
  2. Shouldn’t this be try expect(values.len == 10); Nevermind - PEMDAS mistake on my part.
test "tuple" {
    const values = .{
        @as(u32, 1234),
        @as(f64, 12.34),
        true,
        "hi",
    } ++ .{false} ** 2;
    try expect(values[0] == 1234);
    try expect(values[4] == false);
    inline for (values) |v, i| {
        if (i != 2) continue;
        try expect(v);
    }
    try expect(values.len == 6);
    try expect(values.@"3"[0] == 'h');
}
Enter fullscreen mode Exit fullscreen mode
  1. Sentinel terminated arrays and slices appear to exist primarily for c interop.
  2. Vector types are super cool.

Standard patterns

  1. createFile allocates resources but makeDir doesn’t. Why the inconsistency?
  2. Many deallocations appear to be missing from this code. In a normal function, would they be automatically deallocated when they go out of scope or does this result in a memory leak or illegal behavior?
test "make dir" {
    try std.fs.cwd().makeDir("test-tmp");
    const dir = try std.fs.cwd().openDir(
        "test-tmp",
        .{ .iterate = true },
    );
    defer {
        std.fs.cwd().deleteTree("test-tmp") catch unreachable;
    }

    _ = try dir.createFile("x", .{});
    _ = try dir.createFile("y", .{});
    _ = try dir.createFile("z", .{});

    var file_count: usize = 0;
    var iter = dir.iterate();
    while (try iter.next()) |entry| {
        if (entry.kind == .File) file_count += 1;
    }

    try expect(file_count == 3);
}
Enter fullscreen mode Exit fullscreen mode
  1. The formatting examples show a format fn defined in a struct. Is this common or idiomatic? I mean… is there anything like Java’s toString or Rust’s debug trait that’s ubiquitous. EDIT: json stringify looks promising.
  2. The first example in the json subsection parses a map without an allocator which appears to invalidate this statement. which is true?

The json parser requires an allocator for javascript’s string, array, and map types. This memory may be freed using [std.json.parseFree](https://ziglang.org/documentation/master/std/#std;json.parseFree).

  1. Iterator return types seem well thought-out.
  2. This chapter is missing a lot.
  3. Any plans for atomic operations on shared mutable state (eg atoms in Clojure)?

Working with C

I didn’t read through this section but I wonder if you could integrate Guile scheme with no issues. That sounds like an effective heat check.

Async

I skimmed this section and my initial thoughts are

  1. Coroutines are awesome but ridiculously hard to develop
  2. I’m getting some “what color is your function?” vibes.
  3. I’ll bet this stack manip upsets the guile scheme integration idea.
  4. Maybe futures/promises would have been sufficient.

Closing thoughts

  1. Overall I still have too many questions and feel a bit uneasy about using it.
  2. Somewhere I read that Zig avoids truthy/falsy values but that appears to be false.
  3. I suspect most of the footguns hide in the ability to dereference any value passed to a function.
  4. There are probably a fair number of footguns w/ arrays & slices passed to and returned from functions.
  5. const is encouraged but too nuanced to not screw up once in a while.
  6. While impressive, coroutines might not be the right approach to async.
  7. I’m surprised it didn’t go over closures and higher-level functions.
  8. While looking around for benchmarks, a hn post directed me to these 2 tickets. One of Zig’s biggest promises has asterisks. Quick gut check for anyone reading: what’s your prediction for these 2 tickets?

https://github.com/ziglang/zig/issues/1966

https://github.com/ziglang/zig/issues/2301

Oldest comments (0)