Inconsistency when undefined is assigned to optional

The following code behaves differently depending on the whether runtime safety is active:

const std = @import("std");

pub fn main() !void {
    var a: ?u32 = @as(?u32, undefined);
    const b = &a.?;
    b.* = 0x1234;
    std.debug.print("{?x}\n", .{a});
}

When optimize is Debug or ReleaseSafe, the output is 1234. When it’s ReleaseSmall or ReleaseFast, we get null instead. Should this be reported?

You are relying on unchecked illegal behavior, so this code isn’t valid and the bugs are your own.

Consider this modification to your program:

const std = @import("std");

pub fn main() !void {
    var a: ?u32 = @as(?u32, undefined);
    std.debug.print("{X}\n", .{std.mem.asBytes(&a)});
    const b = &a.?;
    b.* = 0x1234;
    std.debug.print("{X}\n", .{std.mem.asBytes(&a)});
    std.debug.print("{?x}\n", .{a});
}

This is the output with Debug.

AAAAAAAAAAAAAAAA
34120000AAAAAAAA
1234

and here it is with release fast:

0000000000000000
3412000000000000
null

So what’s going on here? The definition of “undefined” is that it can take on any value. Almost always, this means that you should never read an undefined value, only later write to it before any reads take place. In safe modes, Zig will write hex ‘AA’ over any memory set to undefined, as a marker for the programmer doing the debugging that if you’re looking at that, it’s /probably/ undefined memory. In release mode, this overwriting doesn’t happen.

So in your code, you’re setting the whole optional to undefined. In memory, the u32 comes first, and the flag for whether the optional is set to a value comes second. The actual illegal code is const b = &a.?;. In Debug, it will check that the optional is set, which reading the flag, it happens to see a non-zero value, and thinks it is. In ReleaseFast, it will doesn’t safety check the presence flag, which happens to default to zero’d memory, but wouldn’t always.

I suspect that what you intended to do was:

const std = @import("std");

pub fn main() !void {
    var a: ?u32 = @as(u32, undefined);
    std.debug.print("{X}\n", .{std.mem.asBytes(&a)});
    const b = &a.?;
    b.* = 0x1234;
    std.debug.print("{X}\n", .{std.mem.asBytes(&a)});
    std.debug.print("{?x}\n", .{a});
}

Which in Debug prints:

0000000001000000
3412000001000000
1234

and in ReleaseFast prints:

0000000001000000
3412000001000000
1234
2 Likes

No, because it works according to the documentation.
Using a value set to undefined is a bug. You can use undefined to defer value initialization or to not use a variable at all.

undefined means the value could be anything, even something that is nonsense according to the type. Translated into English, undefined means “Not a meaningful value. Using this value would be a bug. The value will be unused, or overwritten before being used.”
Documentation - The Zig Programming Language

2 Likes

All that said, I /think/ this could be checked illegal behavior, instead of being unchecked. I’ve asked the compiler team for their opinion. I’m also surprised var a: ?u32 = @as(u32, undefined); doesn’t result in the memory being AAAAAAAA01000000 in Debug, which I’ve also asked about.

There’s an issue tracking “check 0 and 1, not 0 and not-0, in safe modes”, came out of a similar conversation here on the forum.

If the behavior is changed to do the check more specifically, we wouldn’t want that IMHO. undefined is undefined, code shouldn’t look at it, and a nullness check is looking.

I get the perspective that using @as(u32, undefined) is kind of like setting one field but not another, I don’t think it’s a good approach to optionals because of how they participate in control flow. It’s like saying “well technically, this could be either type, who knows, but actually it’s definitely a u32 I just don’t know which one”. Whatever bug is involved in creating that definition, setting the tag to “Some” won’t help fix it.

I have never had a situation where I needed an initial value for a ? which couldn’t be null. ?T says “maybe there’s something here, maybe there isn’t”, well, if there isn’t something on the RHS, then there isn’t a something there, so null it is.

1 Like

Consistency across different optimization settings is reasonable expectation if you ask me. The problem with something like this is that people will go out of their way to use it once discovered.