Need Help Finding Heisenbug

Hi,
I have a code (It solves Advent of Code 2015 day04 part 2, spoiler warning!) and it behaves weirdly in ReleaseFast mode. Debug mode is fine.

build.zig
const std = @import("std");

// Although this function looks imperative, note that its job is to
// declaratively construct a build graph that will be executed by an external
// runner.
pub fn build(b: *std.Build) void {
    // Standard target options allows the person running `zig build` to choose
    // what target to build for. Here we do not override the defaults, which
    // means any target is allowed, and the default is native. Other options
    // for restricting supported target set are available.
    const target = b.standardTargetOptions(.{});

    // Standard optimization options allow the person running `zig build` to select
    // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. Here we do not
    // set a preferred release mode, allowing the user to decide how to optimize.
    const optimize = b.standardOptimizeOption(.{});

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

    // This declares intent for the executable to be installed into the
    // standard location when the user invokes the "install" step (the default
    // step when running `zig build`).
    b.installArtifact(exe);

    // This *creates* a Run step in the build graph, to be executed when another
    // step is evaluated that depends on it. The next line below will establish
    // such a dependency.
    const run_cmd = b.addRunArtifact(exe);

    // By making the run step depend on the install step, it will be run from the
    // installation directory rather than directly from within the cache directory.
    // This is not necessary, however, if the application depends on other installed
    // files, this ensures they will be present and in the expected location.
    run_cmd.step.dependOn(b.getInstallStep());

    // This allows the user to pass arguments to the application in the build
    // command itself, like this: `zig build run -- arg1 arg2 etc`
    if (b.args) |args| {
        run_cmd.addArgs(args);
    }

    // This creates a build step. It will be visible in the `zig build --help` menu,
    // and can be selected like this: `zig build run`
    // This will evaluate the `run` step rather than the default, which is "install".
    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);

    // Similar to creating the run step earlier, this exposes a `test` step to
    // the `zig build --help` menu, providing a way for the user to request
    // running the unit tests.
    const test_step = b.step("test", "Run unit tests");
    test_step.dependOn(&run_exe_unit_tests.step);
}
main.zig
const std = @import("std");
const stdout = std.io.getStdOut().writer();
const stderr = std.io.getStdErr().writer();

pub fn startsNZerosHex(inp: []const u8, n: usize) bool {
    if (inp.len < @divFloor(n + 1, 2)) {
        return false;
    }
    const n_is_even = @mod(n, 2) == 0;
    const nev = if (n_is_even) n else n - 1;

    for (inp[0..@divExact(nev, 2)]) |i| {
        if (i != 0) {
            return false;
        }
    }

    if (!n_is_even) {
        return inp[@divExact(nev, 2)] < 16;
    }
    return true;
}

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

    const args = try std.process.argsAlloc(allocator);
    defer std.process.argsFree(allocator, args);
    if (args.len < 2) {
        try stderr.print("Usage: {s} <filename>\n", .{args[0]});
        return error.InvalidArguments;
    }
    const filename = args[1];
    const file = try std.fs.cwd().openFile(filename, .{});
    defer file.close();
    const file_size = try file.getEndPos();

    const input = try allocator.alloc(u8, file_size);
    defer allocator.free(input);

    const bytes_read = try file.readAll(input);
    if (bytes_read != file_size) {
        try stderr.print("Warning: Read {d} bytes but expected {d}\n", .{ bytes_read, file_size });
    }

    // try stdout.print("File input is {s}\n", .{input}); // including this line ommits the error message.

    var md5input = std.ArrayList(u8).init(allocator);
    defer md5input.deinit();

    const Md5 = std.crypto.hash.Md5;

    var output: [Md5.digest_length]u8 = undefined;

    var num: usize = 0;
    while (!startsNZerosHex(&output, 6)) : (num += 1) {
        md5input.clearRetainingCapacity();
        _ = try md5input.writer().print("{s}{d}", .{ input[0 .. file_size - 1], num });
        Md5.hash(md5input.items, &output, .{});
    }
    num -= 1;

    try stdout.print("md5(\"{s}\") = ", .{md5input.items});
    for (output) |i| {
        try stdout.print("{x:0>2}", .{i});
    }
    try stdout.print(" -> {d}\n", .{num});
}
test.inp
bgvyzdsv

When I run it regularly it results in:

$ zig build run -- ./src/test.inp 
md5("bgvyzdsv1038736") = 000000b1b64bf5eb55aad89986126953 -> 1038736

When I run it with ReleaseFast:

$ zig build -Doptimize=ReleaseFast run -- ./src/test.inp 
Warning: Read 9 bytes but expected 9

and then hangs in an infinite loop.
In line 44 I test if the read bytes and the file size match. If they do not I will print out the values. However, as you can see in the above output, the read bytes and file size match, but the output is written anyway.

Interestingly if I remove everything after line 48 the warning is not printed anymore. Also if I uncomment line 48 where I print the file contents.
Sorry, but I could not find a way to reduce the code without this behavior disappearing.

What did I do wrong? And If I did something wrong, why does it work in debug mode but not in ReleaseFast mode?

Thanks for your help.

First time you call startsNZerosHex it’s with undefined data, isn’t it? Next round it has the hash output.

1 Like

Thank you,
I initialized the output array

var output: [Md5.digest_length]u8 = ([_]u8{1}) ** Md5.digest_length;

and the error disappeared.
But since this should be executed after parsing the and checking the file, how could this effect the if and, why is the expression bytes_read != file_size true, if the print statement shows the values to be different?

In Debug mode, the output array was initialized to all 0xAA’s (because of = undefined). I’d go through to code and see why it happened to work with that.

Reading undefined values is undefined behaviour. The compiler assumed that this code path would be unreachable:

It therefore assumed that the true branch here:

Would always be taken, and that print would either never return or return an error. Neither happened, and the code probably drifted into an infinite loop.

3 Likes