Sotolf's thoughts and oddities

Zig Freeing Error

I’m still working on learning zig, and today I had some issues with a small parser that I was writing in my quest to learn enough of it to be a bit more comfortable with the language.

I struggled a bit with how to create a parser, thinking first that I would have to create a mutable string to be able to chop off bits of it as it got parsed. I ended up scrapping that idea when I read some example code for a toml parser that did the obvious, a struct that just points to the string and an index, so wohoo, working well so far, I could read my nested lists into structs, these looks like this:

 1const List = struct {
 2    elements: []Element,
 3
 4    fn free(self: *List, alloc: mem.Allocator) void {
 5        //print("freeing: {any}\n", .{self});
 6        for (self.elements) |*elem| {
 7            switch (elem.*) {
 8                .num => continue,
 9                .list => |*lst| lst.*.free(alloc),
10            }
11        }
12        alloc.free(self.elements);
13    }
14};
15
16
17const Element = union(enum) {
18    num: usize,
19    list: List,
20};

So my list is basically a slice of elements that can be either a number or another list. When I want to free out the memory I recurse into the sublists and free them from the inside out.

This all worked quite well, at least in theory, when I parsed my file into this struct, the compiler was yelling at me that I was leaking memory. I tried a lot of things and couldn’t really wrap my head around what it was that was wrong.

Some people may be able to see from my parsing code that will follow here, but it took me quite a while to figure it out. So here we go:

 1fn getLines(path: []const u8, alloc: mem.Allocator) !std.ArrayList(Pair) {
 2    const file = std.fs.cwd().openFile(path, .{}) catch |err| {
 3        std.log.err("Failed to open file {s}", .{@errorName(err)});
 4        return err;
 5    };
 6    defer file.close();
 7
 8    var result = std.ArrayList(Pair).init(alloc);
 9    var first: ?List = null;
10    var second: ?List = null;
11
12    while (file.reader().readUntilDelimiterOrEofAlloc(alloc, '\n', std.math.maxInt(usize)) catch |err| {
13        std.log.err("Failed to read line: {s}", .{@errorName(err)});
14        return err;
15    }) |line| {
16        defer alloc.free(line);
17        if (mem.eql(u8, line, "")) {
18            try result.append(.{ .first = first.?, .second = second.? });
19            first = null;
20            second = null;
21            continue;
22        }
23        var parser = Parser.fromString(line);
24        if (first == null) {
25            first = try parser.parseList(alloc);
26        } else {
27            second = try parser.parseList(alloc);
28        }
29    }
30
31    return result;
32}

The compiler says that the parser was allocating lists that didn’t get freed, and I could not quite wrap my head around why, so I started to debug my parser, but everything looked fine there, it wasn’t leaking, and I was going through my file by lines, and it worked fine, no leakage at all, until I reached the last pair of the file.

And then it hit me, there is no empty line at the end of the file, so the last pair that I prepare never gets added to the result. And since it never was a part of the result it never got freed and just leaked all over the place. Basically all I had to do was add a little:

1    if (first != null and second != null) {
2        try result.append(.{ .first = first.?, .second = second.? });
3    }

to make sure that the last lines in the file also will get added, if they are there. And it worked!

I think this is the first time that leaking memory actually saved a logical problem in the code for me. Now of course it depends on you using a language with leak detection, but I was very happy to figure it out.

I’m still having a lot of fun writing zig. And there are a lot of small learning opportunities like this, slowly I’m getting less horrible at this, and having a great time doing it.