Is this an zig optimization bug?

Hi, im trying to learn zig it’s my first system programing language. Im starting with a dumb cat program. however i think i found a bug when i do -Doptimize=ReleaseFast.

This is my code:

const std = @import("std");
const process = std.process;
const stdout = std.io.getStdOut().writer();

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

    const args = try process.argsAlloc(ally);
    defer process.argsFree(ally, args);

    if (args.len == 1) {
        std.debug.print("Not file provided\n", .{});
        process.exit(1);
    }

    var status: u8 = 0;
    for (args[1..]) |arg| {
        var splits = std.mem.splitBackwardsAny(u8, arg, "/");
        const title = splits.first();

        const file = std.fs.cwd().openFile(arg, .{}) catch |err| {
            std.debug.print("{s}: Failed to open file - {any}\n", .{ title, err });
            status = 1;
            continue;
        };
        defer file.close();

        try stdout.print("{s}:\n", .{title});

        var buffered_file = std.io.bufferedReader(file.reader());

        while (true) {
            var buffer: [128]u8 = undefined;

            const number_of_read_bytes = buffered_file.read(&buffer) catch |err| {
                std.debug.print("{s}: Failed to read file - {any}\n", .{ title, err });
                status = 1;
                break;
            };

            if (number_of_read_bytes == 0) {
                break;
            }

            try stdout.print("{s}", .{buffer});
        }
    }

    process.exit(status);
}

And this is the build file:

const std = @import("std");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});

    const optimize = b.standardOptimizeOption(.{});

    const exe = b.addExecutable(.{
        .name = "zig-cat",
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    b.installArtifact(exe);

    const run_cmd = b.addRunArtifact(exe);

    run_cmd.step.dependOn(b.getInstallStep());

    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    const run_step = b.step("run", "Run the app");
    run_step.dependOn(&run_cmd.step);

    const exe_unit_tests = b.addTest(.{
        .root_source_file = b.path("src/main.zig"),
        .target = target,
        .optimize = optimize,
    });

    const run_exe_unit_tests = b.addRunArtifact(exe_unit_tests);

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

When i build with -Doptimize=ReleaseFast and try to cat a file.
for example:

Lorem, ipsum dolor sit amet consectetur adipisicing elit.
Rem earum nostrum quisquam asperiores omnis id officia odit? Laboriosam,
perferendis voluptatum porro necessitatibus mollitia,
liquid eum ex expedita iste dolores minima?

This is what the program print out in -Doptimize=ReleaseFast.

test.txt:
Lorem, ipsum dolor sit amet consectetur adipisicing elit.
Rem earum nostrum quisquam asperiores omnis id officia odit? Laboriosam,
perferendis voluptatum porro necessitatibus mollitia,
liquid eum ex expedita iste dolores minima?
 id officia odit? Laboriosa%

While in debug mode and safe mode it print like the original file.
I think the buffer variable does not drop when the scope ends. idk if it’s a bug on my end or in zig.

Try with:

try stdout.print("{s}", .{buffer[0 .. number_of_read_bytes]});
2 Likes

Thanks ,that works. Still, do you know why buffer does not get dropped when we continue the while loop and a new buffer get created?

Variable buffer is a local variable, so it is theoretically a new instance on each iteration of the while loop. But the compiler doesn’t actually have to do anything with it, because it is not being initialized, so the old value is reused each time. You could try setting it to all zeros with:

var buffer = [_]u8{0} ** 128;

but that might / will have some (negligible?) runtime impact. It is better to just use the part of buffer that is actually being filled by the read() call, by slicing it.

2 Likes

Welcome to Ziggit!

buffer is a stack variable: a specific region of memory allocated with the rest of the function. It doesn’t have an ‘identity’ in the way you might be used to from other languages.

Since it lives on the stack and it’s scoped within the while loop, the compiler reuses that region of memory each time the while loop executes.

In debug mode (I think this is what’s happening, not 100% about this part) the compiler emits code to fill the buffer with 0xaaaaaaaaaaa. That doesn’t correspond to any valid sequence of text, so it’s getting ‘printed’, but the terminal doesn’t display it.

2 Likes

First thing i thought is that’s a compiler bug, i skill issued myself. :joy::joy::joy::joy:

1 Like

We’re here to help :slight_smile:

Systems programming is different from what you’re used to, and part of that is the entire idea of build modes. So you see a program which behaves differently in different build modes, it’s logical that you thought of that.

But Debug and ReleaseSafe, on the one hand, and ReleaseFast and ReleaseSmall on the other, can and do differ between them, but only (ideally!) when a program exhibits illegal behavior. In this case, you read some undefined memory. The compiler / debug runtime can catch some cases of reading undefined memory, but it isn’t fine-grained enough to detect this case: you put something in the buffer before you printed it, which coerced the entire buffer to u8, even the stuff which isn’t supposed to be read from.

The number of compiler bugs in Zig is not zero, it’s a pre-1.0 language after all. But in this case, you need to slice the buffer by the number of bytes, or you’ll get whatever else is in there when it’s partially filled.

2 Likes