zig icon indicating copy to clipboard operation
zig copied to clipboard

Inline if for variable bool value makes the result a reference/pointer to the data

Open samhattangady opened this issue 1 year ago • 4 comments

Zig Version

0.11.0-dev.3395+1e7dcaa3a

Steps to Reproduce and Observed Behavior

When we use an inline if, the result value is a pointer to the data.

So if we say

var condition1 = true;
const ans1 = if (condition1) answers[0] else answers[1]

then ans1 will be a reference to answers[0], and if we change answers[0], then value in ans1 will also change.

This does not happen if condition1 is declared as const.

I initially caught the bug in version 2939, with variable optional types, but that has been fixed since.

Reproducible example:

const std = @import("std");

const Block = struct {
    value: u8,
};

pub fn main() !void {
    var blocks: [16]Block = undefined;
    {
        blocks[0] = .{ .value = 0 };
        // using variable optionals and bools
        var variable_opt_index: ?usize = 0;
        var variable_bool: bool = true;
        const variable_opt_index_block = if (variable_opt_index) |i| blocks[i] else blocks[1];
        const variable_bool_index_block: Block = if (variable_bool) blocks[0] else blocks[1];
        // using constant optionals and bools
        const const_opt_index: ?usize = 0;
        const const_bool: bool = true;
        const const_opt_index_block = if (const_opt_index) |i| blocks[i] else blocks[1];
        const const_bool_index_block = if (const_bool) blocks[0] else blocks[1];
        const opt_value = blocks[0].value;
        // changing the underlying value
        blocks[0].value = 42;
        std.debug.print("opt_value = {d}\n", .{opt_value});
        std.debug.print("variable_opt_index_block.value = {d} // should be 0\nvariable_bool_index_block.value = {d} // should be 0\n", .{
            variable_opt_index_block.value, variable_bool_index_block.value,
        });
        std.debug.print("const_opt_index_block.value = {d}\nconst_bool_index_block.value = {d}\n", .{
            const_opt_index_block.value, const_bool_index_block.value,
        });
    }
}

Output:

opt_value = 0
variable_opt_index_block.value = 0 // should be 0
variable_bool_index_block.value = 42 // should be 0
const_opt_index_block.value = 0
const_bool_index_block.value = 0

Expected Behavior

The value returned should not be a reference. So changing the underlying data should not change the value stored.

Expected output for the above code:

opt_value = 0
variable_opt_index_block.value = 0 // should be 0
variable_bool_index_block.value = 0 // should be 0
const_opt_index_block.value = 0
const_bool_index_block.value = 0

samhattangady avatar Jun 12 '23 11:06 samhattangady

wierd. here's a reduction in the form of a failing test:

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

const S = struct { value: u8 };

test {
    var buf: [16]S = undefined;
    buf[0] = .{ .value = 0 };

    var x = true;
    const s = if (x) buf[0] else buf[1];
    buf[0].value = 42;

    try std.testing.expectEqual(@as(u8, 0), s.value);
}
$ zig test /tmp/tmp.zig 
Test [1/1] test_0... expected 0, found 42
Test [1/1] test_0... FAIL (TestExpectedEqual)
/media/data/Users/Travis/Documents/Code/zig/zig/lib/std/testing.zig:78:17: 0x20f19e in expectEqual__anon_1096 (test)
                return error.TestExpectedEqual;
                ^
/tmp/tmp.zig:13:5: 0x20f41e in test_0 (test)
    try std.testing.expectEqual(@as(u8, 0), s.value);
    ^
0 passed; 0 skipped; 1 failed.

travisstaloch avatar Jun 12 '23 12:06 travisstaloch

I have seen this behavior while working on the compiler, and I had to use var instead of const to force a copy to be made, even though I never needed to modify the copy.

jacobly0 avatar Jun 12 '23 15:06 jacobly0

I am currently using the fix mentioned by @jacobly0 to get around the issue.

Though I am not able to create a small reproducible test case, I am facing this same issue with optional values in my codebase.

samhattangady avatar Jun 13 '23 03:06 samhattangady

Just got bit by this bug, quite nasty as it presents very similar to undefined behavior.

I initially caught the bug in version 2939, with variable optional types, but that has been fixed since.

Doesn't seem like it, here's a minimal reproducible example

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

pub fn do_stuff(cond: bool) ?usize {             
    var opt: ?usize = 1;                         
    const copy = if (cond) opt else return null; 
    opt = null; 
                                                                 
    return copy;
}
                                      
pub fn main() !void {                                         
    try expect(do_stuff(true).? == 1);      
}

(link to a runnable example, we expect the test in main to pass. Also it doesn't matter what value you assign to opt, copy ends up taking on the same value. Version 0.12.0-dev.1849+bb0f7d55e)

Also take a look at this comment here. Most notably, copy in the above snippet does not have the same address as opt, yet assigning to opt changes the observable value in copy.

Bryysen avatar Dec 26 '23 01:12 Bryysen