2025-01-16 20:47:58 -08:00

12 KiB

Notes, thoughts, or questions that arose as I implemented the solutions. Hopefully I am able to go back and answer these questions as I keep learning!

Useful references

  • Zig Notes - particularly on Arrays vs. Slices, and Strings.

Things I like

  • Continue expressions
  • Built in optionals (with orelse)
  • Better error-handling than GoLang's (though that bar is set real low). I have only just scratched the surface, though, it looks interestingly powerful - might well be even better than I've realized at this point!
  • Creation of "bare" structs - i.e. you can do myFunction(.{thing}) rather than myFunction(StructName{thing}) (looking at you, GoLang)
  • Continue expressions - don't need to remember to put the index-incrementing code at the end of every branch!
  • Great powerful switch syntax (though not as powerful as Rust's)
  • Labelled loops - usually should be avoided, but helpful on occasion!
  • defer - though, unlike in GoLang where it's a nice-to-have that allows one to do cleanup, here it is absolutely essential for all the manual deinits and frees

Things that I've found missing from this language

Hmmmm, right now it seems even worse than GoLang. Though the Error handling is so much better that I can forgive much of this (which can be hacked-in to personal taste with utility functions, whereas you cannot fix GoLang's Errors as they are built-in language features).

Not "missing", but...

It irritates me that Zig - like GoLang - has continued C's demonstrably-incorrect inversion of the addressing/dereferencing operators. If *T is the type-symbol for "a pointer to a type T", then, for a value t, the symbol for "a pointer to the value t" should be *t, not &t.

Plus, needing semi-colons on the end of every line? Come on, my guy.

Questions

(From Ziglings)

Problem 40 says "You can always make a const pointer to a mutable value (var), but you cannot make a var pointer to an immutable value (const)." - which, sure, fair enough (I mean, not really, but I'm not going to argue with it...), but that's not what's presented in the problem - the original code is:

    const a: u8 = 12;
    const b: *u8 = &a; // fix this!

which is a constant pointer to a constant value - which shouldn't be an issue?

What's the idiomatic way to run Zig tests?

I've tried - using references like this, this, and this - to set up build.zig so that I could run zig build test and thus run every test in my files, but that didn't work:

// build.zig
const std = @import("std");

pub fn build(b: *std.Build) void {
    const main_tests = b.addTest(.{ .root_source_file = b.path("main.zig") });
    const build_mode = b.standardReleaseOptions();
    main_tests.setBuildMode(build_mode);

    const test_step = b.step("test", "Run library tests");
    test_step.dependOn(&main_tests.step);
}

// main.zig
pub const one = @import("solutions/01.zig");

test {
    @import("std").testing.refAllDecls(@This());
}

As-written, zig build test gives:

/Users/scubbo/Code/advent-of-code-2024/build.zig:19:25: error: no field or member function named 'standardReleaseOptions' in 'Build'
    const build_mode = b.standardReleaseOptions();

With that line (and the following one deleted), zig build test completes silently, even with a failing test.

And this setup still isn't great, because it's necessary to manually import every file to main.zig's imports.

Hence the test.sh workaround script. It's not great, because it will error-out on the first failure (rather than accumulating failures from all files) - but it does the job!

Refer to here for more info - which I only found after writing that hacky script.

Why can't a string-literal be passed to a function that accepts a []8?

That is, why is this illegal?

fn doIt(string: []u8) []u8 {
    return "prefix" + string;
}

const expect = @import("std").testing.expect;

test {
    expect(std.mem.eql(u8, doIt("foo"), "prefixfoo"));
}

I can fix it by changing the type signature to accept []const u8, but (I think?) that then means that I can't call the function with non-const-length strings - including strings read from files.

This link refers to []const u8 as a "Zig-style string-slice", but also refers to [*c]const u8 as a "c_string", so...🤷?


Further questioning on this here.

Why can't I iterate over a HashMap?

The following code:

const std = @import("std");
const print = std.debug.print;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();
    var hashMap = std.AutoHashMap(u32, u32).init(allocator);

    try hashMap.put(2, 5);
    try hashMap.put(1, 35);
    try hashMap.put(4, 20);

    const iter = hashMap.keyIterator();
    while (try iter.next()) |key| {
        print("{}\n", .{key});
    }
}

gives:

scratch.zig:15:20: error: expected type '*hash_map.HashMapUnmanaged(u32,u32,hash_map.AutoContext(u32),80).FieldIterator(u32)', found '*const hash_map.HashMapUnmanaged(u32,u32,hash_map.AutoContext(u32),80).FieldIterator(u32)'
    while (try iter.next()) |key| {
               ~~~~^~~~~
scratch.zig:15:20: note: cast discards const qualifier
/Users/scubbo/zig/zig-macos-x86_64-0.14.0-dev.2362+a47aa9dd9/lib/std/hash_map.zig:894:35: note: parameter type declared here
                pub fn next(self: *@This()) ?*T {

I think this means that the pointer to the Iterator is a Const-pointer and .next() expects a mutable pointer. But, if so - how do we get a mutable pointer from a const? I tried @ptrCast but that gave a similar error.

How to return items accumulated into an ArrayList without causing a memory leak or a segementation fault?

Trimming down the issues that I first saw in problem 05, here's some example code:

const std = @import("std");
const print = std.debug.print;

test "Demo accumulation" {
    const accumulated = try accumulate();
    print("DEBUG - accumulated values are {any}\n", .{accumulated});
}

fn accumulate() ![]u32 {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var list = std.ArrayList(u32).init(allocator);
    // defer list.deinit(); <-- this is the problem line
    try list.append(1);
    try list.append(2);
    try list.append(3);
    return list.items;
}

If the "problem line" is commented out, then I get warnings about a memory leak (unsurprisingly); but if it's left in, then I get a segmentation fault when trying to reference the response of the function.

This is all, in some sense, "working as expected" (the compiler is correct to warn about the memory leak) - but it seems like a cumbersom way to work. I suspect that the response would be "don't return a bare []u32, then", which feels pretty unsatisfying.

You can't even work around this by creating a buffer (within accumulate), copying values into it, deinit-ing list, and returning the copy - because you can't create an array-buffer without pre-specifying how large it should be, and creating a slice has the same memory-leak issue - see below for example:

test "Demo accumulation" {
    const accumulated = try accumulate();
    print("DEBUG - accumulated values are {any}\n", .{accumulated});
}

fn accumulate() ![]u32 {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var list = std.ArrayList(u32).init(allocator);
    defer list.deinit();
    try list.append(1);
    try list.append(2);
    try list.append(3);

    const response = try allocator.alloc(u32, list.items.len);
    @memcpy(response, list.items);
    return response;
}

What's the point in HashMap.getOrPut?

getOrPut doesn't actually put anything, it only gets. See https://ziggit.dev/t/whats-the-point-in-hashmap-getorput/7547.


Draft text of Ziggit post - don't check this in!

Sorry, folks, I'm still not getting it.

Here's a much simpler example:

const std = @import("std");
const print = std.debug.print;

const expect = @import("std").testing.expect;

const Value = struct {
    inner_value: u32,

    pub fn increment(self: *Value) void {
        self.inner_value = Value{ .inner_value = self.inner_value + 1 };
    }

    pub fn from_line(line: []const u8) !Value {
        return Value{ .inner_value = try std.fmt.parseInt(u32, line, 10) };
    }
};

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Pretend that this string was being read from a file.
    const input_data = "2\n10";

    var it = std.mem.splitScalar(u8, input_data, '\n');
    var values = std.ArrayList(Value).init(allocator);
    defer values.deinit();

    while (it.next()) |line| {
        try values.append(try Value.from_line(line));
    }

    for (values.items) |value| {
        value.increment();
    }
    print("{}\n", .{values.items[0].inner_value});
    try expect(values.items[0].inner_value == 3);
    try expect(values.items[1].inner_value == 11);
}

(fiddle)

This gives an error on the line value.increment(), with text expected type '*<uuid>.Value', found *const <uuid>.Value'. It looks like from_line is returning a const value, which thus has immutable fields. It seems to be illegal syntax to declare from_line as returning var !Value. If I instead try explicitly assigning the values to a var identifier:

...
    while (it.next()) |line| {
        var val = try Value.from_line(line);
        try values.append(val);
    }
...

I get a syntax error local variable is never mutated.

If I try making from_line return a pointer (fiddle), I get an error that the return type of from_line is incorrect - expected type '...!*<uuid>.Value', found '*const <uuid>.Value'. But if I introduce an intermediate var variable (fiddle), a bunch of confusing stuff happens:

  • Firstly - the code actually runs. I don't understand why var foo = <expression>; return &<expression> would be any different than return &<expression>; and, moreover, why isn't this complaining that a local variable isn't mutated? I never change val within the scope of from_line.
  • Secondly, the results are both unexpected and inconsistent:
    • In ZigFiddle, I get a printed 12, which is equal to neither 2+1 nor 10+1. The expects do not fail.
      • It is equal to 10 + 1 + 1, so maybe the second Value was the one that got incremented both times? I can't see how that could be possible, though. And, when I tried adding a third value, the result was 8, so that theory doesn't hold water.
    • On my own machine, I get the value 32761 printed, which is so far from either of those values that it makes me suspect I've somehow printed a bare pointer by mistake, but I can't see where I've done so if that's the case.