Pointer changes unexpectedly

I am new to Zig so idk if I’m missing something basic here, but the behaviour seems strange all the same.

I have moved my tests into a main function just to simplify testing with lldb.

I have the following code:

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

pub fn LinkedList(comptime T: type) type {
    return struct {
        pub const Node = struct {
            node_prev: ?*Node,
            node_next: ?*Node,
            data: T,
        };

        node_first: ?*Node,
        node_last: ?*Node,
        len: usize,

        fn append(self: *LinkedList(T), data: T) usize {
            if (self.len == 0) {
                var node = Node{
                    .node_next = null,
                    .node_prev = null,
                    .data = data,
                };

                self.node_first = &node;
                self.node_last = &node;
            } else {
                var node = Node{
                    .node_next = null,
                    .node_prev = self.node_last,
                    .data = data,
                };
                const last_ptr = &(self.node_last orelse unreachable);
                last_ptr.*.node_next = &node;
                self.node_last = &node;
            }

            self.len += 1;

            return self.len;
        }

        fn get(self: *LinkedList(T), index: usize) error{IndexOutOfBounds}!T {
             if (index >= self.len) {
                 return error.IndexOutOfBounds;
             } else {
                 var node_ptr = &(self.node_first orelse unreachable);
                 if (index > 0) {
                     for (0..index) |_| {
                         node_ptr = &(node_ptr.*.node_next orelse unreachable);
                     }
                 }
                 return node_ptr.*.data;
             }
         }
     };
 }

 pub fn main() !void {
     // TEST APPEND
     var list = LinkedList(usize){
         .node_first = null,
         .node_last = null,
         .len = 0,
     };

     try expect(list.len == 0);
     try expect(list.node_first == null);
     try expect(list.node_last == null);

     _ = list.append(10);
     try expect(list.len == 1);
     var node = list.node_first orelse unreachable;
     try expect(node.data == 10);
     node = list.node_last orelse unreachable;
     try expect(node.data == 10);

     _ = list.append(2);
     try expect(list.len == 2);
     node = list.node_first orelse unreachable;
     try expect(node.data == 10);
     node = list.node_last orelse unreachable;
     try expect(node.data == 2);

     for (0..10) |i| {
         _ = list.append(i);
         node = list.node_last orelse unreachable;
         try expect(node.data == i);
     }

     try expect(list.len == 12);

     // TEST GET
     var list2 = LinkedList(i32){
         .node_first = null,
         .node_last = null,
         .len = 0,
     };
     try expect(list2.len == 0);
     try expect(list2.node_first == null);
     try expect(list2.node_last == null);

     _ = list2.append(10);
     try expect(list2.len == 1);
     const data_first = try list2.get(0);
     const node2 = list2.node_first orelse unreachable;
     try std.io.getStdOut().writer().print("data_first = {d}\nnode.data = {d}\n", .{ data_f
 rst, node2.data });
     try expect(data_first == node2.data);
     try expect(data_first == 10);

     _ = list2.append(2);
     try expect(list2.len == 2);
     try expect(try list2.get(0) == 10);
     try expect(try list2.get(1) == 2);
 }

First the test output:

data_first = -1554706984
node.data = -1554706984
error: TestUnexpectedResult
/opt/zig0.14/lib/std/testing.zig:580:14: 0x10de25f in expect (linked_list)
    if (!ok) return error.TestUnexpectedResult;
             ^
/home/volin/programs/zig-projects/linked-list/src/main.zig:108:5: 0x10deb4e in main (linked_list)
    try expect(data_first == node2.data);
    ^
data_first = 1338275224
node.data = 1338275224
error: TestUnexpectedResult
/opt/zig0.14/lib/std/testing.zig:580:14: 0x10de25f in expect (linked_list)
    if (!ok) return error.TestUnexpectedResult;
             ^
/home/volin/programs/zig-projects/linked-list/src/main.zig:108:5: 0x10deb4e in main (linked_list)
    try expect(data_first == node2.data);
    ^

So when I step through this code and examine values everything behaves as expected up to this point.

fn get(self: *LinkedList(T), index: usize) error{IndexOutOfBounds}!T {
    if (index >= self.len) {
        return error.IndexOutOfBounds;
    } else { // GOOD HERE
        var node_ptr = &(self.node_first orelse unreachable);
        if (index > 0) { // BAD HERE
            for (0..index) |_| {
                node_ptr = &(node_ptr.*.node_next orelse unreachable);
            }
        }
    return node_ptr.*.data;
}

After initializing node_ptr self.node_first.node_next and self.node_first.data both get replaced with unexpected data.

I only get one image as a new user so enjoy this wonderful graphic with lldb states:

I’m using this as an opportunity to learn, but I also find this behaviour to be unexpected. I’d love to know if there is something I am failing to understand about how zig works. If this seems strange to others I’d also love to know if it’s worth reporting as a bug

Could be wrong, but is the Node you make in append scoped to that function? Therefore not defined later?

2 Likes

I might be misreading, but, perchance, do you actually mean:

var node_ptr = self.node_first orelse unreachable;

orelse unwraps the “optional” possibility; if node_first is real, as it seems to, and you actually want that pointer (…1c8), then I think this is what you want.(?) What you’re getting, I think, is the address to the pointer, and thus not …1c8.

Oh yes, I only saw Illusion’s post after, and hadn’t even looked at append() - that definitely looks bad - the reference to a local that goes out of scope. If this doesn’t account for the specific behavior on the lines you’re referencing, it will certainly bite you elsewhere.

At the end of the day this results in the same behaviour. I had it this way before, and was experimenting with grabbing an address and dereferencing to see if it changed the behaviour in some way but no luck.

I will look into scope. Does zig garbage collected? Could this have something to do with another level of scope being opened or something triggering the node to be garbage collected?

No, zig is not a GC language. If you want that node that is within append() to stick around, you’ll have to allocate it (with an allocator) and properly mirror the allocation with later deallocation. (I say ‘allocate’ and ‘deallocate’, but you can accomplish this on the stack - you just must do it explicitly.) This is the only way you’ll be able to get that node to live beyond “append”. I haven’t looked closely at the whole of your code, but you’ll have to think critically about where that deallocation would belong; if you’re allocating in append(), and if a caller who calls your get() is supposed to assume ownership of the node, then that caller may have to deallocate the node. Or, if nodes don’t leave the list, you could possibly walk the list and deallocate in a .deinit() function on your container class. Or you could use an ArenaAllocator if you know you want to dump the entire bulk of allocated memory all at once, at some strategic end-point. But you have to design this.

1 Like

So the problem was absolutely that I wasn’t explicitly allocating memory for my node. My pointer was invalid and the memory was being overwritten.

I ran this code bellow to test it out.

var list = LinkedList(usize){
    .node_first = null,
    .node_last = null,
    .len = 0,
};
_ = list.append(10);
_ = list.append(2);

const array = [_]i32{100} ** 100000000;

for (array, 0..array.len) |val, i| {
    _ = try std.io.getStdOut().writer().print("{d}:{d}\n", .{ i, val });
    try expect(list.len == 2);
    const node = list.node_first orelse unreachable;
    try expect(node.data == 10);
}
_ = try std.io.getStdOut().write("Done\n");

with the result

0:100
error: TestUnexpectedResult
/opt/zig0.14/lib/std/testing.zig:580:14: 0x18e564df in expect (linked_list)
    if (!ok) return error.TestUnexpectedResult;
             ^
/home/volin/programs/zig-projects/linked-list/src/main.zig:75:9: 0x18e56849 in main (linked_list)
        try expect(node.data == 10);
        ^

The memory was being overwritten, and then triggering the expect()

The problem occurs here: node is an object constructed on the stack, so when this scope ends, node disappears immediately, and the &node pointer immediately becomes a dangling pointer pointing to an invalid object.

Considering that the OP is likely from languages like Java or C#, in Zig, you need to create node on the heap through a heap allocator to get the object construction method familiar to the OP. The object obtained this way can reside on the heap and will not end its lifetime when the scope ends. However, note that because Zig does not have a GC, it needs to be manually destroyed on the heap in the future.