Build failing in fmt.zig when looping through array of structs

I’m working on small program to learn the language, but I’m running into build errors I don’t understand. When I uncomment afor loop, my program fails to build with an error in fmt.zig, and I don’t see the connection between the error and my code. What’s especially mystifying is the compiler is complaining about the readUntilDelimiter call, but this program builds and runs as expected while the for loop is commented out.

This is the part of my main function that’s breaking:

    while (running) {
        try stdout.print("> ", .{});

        _ = try stdin.readUntilDelimiter(&input_buffer, '\n');
        const input: []u8 = std.mem.sliceTo(&input_buffer, '\n');

        // Uncommenting this loop breaks the build
        // for (commands) |command| {
        //     _ = command;
        // }

        try stdout.print("Invalid command: '{s}'\n", .{input});
    }

The definition for the struct and array in question is here:

const Command = struct {
    action: *const fn () anyerror!void,
    name: []const u8,
    description: []const u8,
};

const commands = [_]Command{
    .{ .action = &help, .name = "help", .description = "description for help" },
    .{ .action = &quit, .name = "quit", .description = "description for quit" },
};

Finally, the build error is here:

/opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/fmt.zig:273:17: error: expected . or }, found ' '
                @compileError("expected . or }, found '" ++ unicode.utf8EncodeComptime(ch) ++ "'");
                ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
/opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/fmt.zig:152:55: note: called from here
        const placeholder = comptime Placeholder.parse(fmt[fmt_begin..fmt_end].*);
                                     ~~~~~~~~~~~~~~~~~^~~~~~~~~~~~~~~~~~~~~~~~~~~
referenced by:
    print__anon_3701: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/io/Writer.zig:23:26
    print: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/io.zig:324:47
    dumpCurrentStackTrace: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/debug.zig:189:19
    panicImpl: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/debug.zig:482:17
    default_panic: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/builtin.zig:845:22
    typeErasedWriteFn: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/io.zig:355:41
    any: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/io.zig:350:28
    bufPrint__anon_6172: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/fmt.zig:1768:24
    panicExtra__anon_2950: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/debug.zig:428:33
    panicOutOfBounds: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/builtin.zig:868:25
    readByte: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/io/Reader.zig:232:5
    streamUntilDelimiter__anon_3698: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/io/Reader.zig:210:38
    readUntilDelimiter: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/io/Reader.zig:137:34
    readUntilDelimiter: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/io.zig:169:41
    main: src/main.zig:153:22
    callMain: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/start.zig:511:32
    callMainWithArgs: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/start.zig:469:12
    main: /opt/homebrew/Cellar/zig/0.12.0/lib/zig/std/start.zig:484:12

I tried recreating your example in godbolt but there’s a good chunk of code missing. Please post your entire file here so we can get a bit more context?

Apologies. I didn’t get a smaller repro like I wanted to, but I can share the file I’m working on. There’s a bunch of junk in here that’s probably unrelated, I’m just tinkering with a card game while learning the language.

The whole file and build error is here: Compiler Explorer. I don’t know how long those links are valid for, so I’ve also pasted in my source file below.

If it matters, I’m using zig 0.12.0 on macos, installed via homebrew.

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

const Rank = enum {
    two,
    three,
    four,
    five,
    six,
    seven,
    eight,
    nine,
    ten,
    jack,
    queen,
    king,
    ace,

    pub const name_table = [@typeInfo(Rank).Enum.fields.len][:0]const u8{
        " 2",
        " 3",
        " 4",
        " 5",
        " 6",
        " 7",
        " 8",
        " 9",
        "10",
        " J",
        " Q",
        " K",
        " A",
    };
};

const Suite = enum {
    club,
    diamond,
    spade,
    heart,

    pub const name_table = [@typeInfo(Suite).Enum.fields.len][:0]const u8{
        "♣",
        "♦",
        "♠",
        "♥",
    };
};

const Card = struct {
    suite: Suite,
    rank: Rank,

    pub fn print(self: *const Card) void {
        std.debug.print("{s}{s}", .{ Rank.name_table[@intFromEnum(self.*.rank)], Suite.name_table[@intFromEnum(self.*.suite)] });
    }
};

pub fn printCards(cards: []Card) void {
    var count: u64 = 0;
    std.debug.print("[ ", .{});
    for (0..cards.len) |i| {
        cards[i].print();
        if (i < cards.len - 1) std.debug.print(", ", .{});
        count += 1;
        if (count % 10 == 0) std.debug.print("\n  ", .{});
    }
    std.debug.print(" ]\n", .{});
}

const CardDb = struct {
    cards: []Card,
};

const Deck = struct {
    cards: []Card,
    size: usize = 0,

    pub fn shuffle(self: *Deck, rand: *const std.Random) !void {
        for (0..self.*.size) |i| {
            const swapWith = rand.uintAtMost(usize, self.*.cards.len - i - 1) + i;
            const tmp = self.*.cards[i];
            self.*.cards[i] = self.*.cards[swapWith];
            self.*.cards[swapWith] = tmp;
        }
    }

    pub fn draw(self: *Deck) Card {
        std.debug.assert(self.*.size > 0);

        defer self.*.size -= 1;
        return self.*.cards[self.*.size - 1];
    }
};

const Command = struct {
    action: *const fn () anyerror!void,
    name: []const u8,
    description: []const u8,
};

const commands = [_]Command{
    .{ .action = &help, .name = "help", .description = "description for help" },
    .{ .action = &quit, .name = "quit", .description = "description for quit" },
};

var running = true;

pub fn main() !void {
    // var buffer: [@sizeOf(Card) * 52]u8 = undefined;
    // var fba = std.heap.FixedBufferAllocator.init(&buffer);
    // const allocator = fba.allocator();

    var input_buffer: [100]u8 = undefined;
    const stdin = std.io.getStdIn().reader();
    const stdout = std.io.getStdOut().writer();

    var prng = std.rand.DefaultPrng.init(blk: {
        var seed: u64 = undefined;
        try std.posix.getrandom(std.mem.asBytes(&seed));
        break :blk seed;
    });
    const rng = prng.random();

    const card_db = CardDb{ .cards = init: {
        const num_suites = std.meta.fields(Suite).len;
        const num_ranks = std.meta.fields(Rank).len;
        const num_cards = num_suites * num_ranks;
        var cards: [num_cards]Card = undefined;

        var i: usize = 0;
        for (0..num_suites) |suite| {
            for (0..num_ranks) |rank| {
                cards[i].suite = @enumFromInt(suite);
                cards[i].rank = @enumFromInt(rank);
                i += 1;
            }
        }

        break :init &cards;
    } };

    var deck = Deck{
        .cards = card_db.cards,
        .size = card_db.cards.len,
    };
    try deck.shuffle(&rng);

    try stdout.print("BLACKJACK SIM\n", .{});

    while (running) {
        try stdout.print("> ", .{});

        _ = try stdin.readUntilDelimiter(&input_buffer, '\n');
        const input: []u8 = std.mem.sliceTo(&input_buffer, '\n');

        for (commands) |command| {
            _ = command;
        }

        try stdout.print("Invalid command: '{s}'\n", .{input});
    }
}

pub fn help() !void {
    try std.io.getStdOut().writer().print("HELP:\n", .{});
    for (&commands) |*cmd| {
        try std.io.getStdOut().writer().print("{s: 10<}{s}\n", .{ cmd.name, cmd.description });
    }
}

pub fn quit() !void {
    running = false;
}

// test "all cards appear exactly once after shuffle" {
//     const allocator = std.testing.allocator;
//     var prng = std.rand.DefaultPrng.init(blk: {
//         var seed: u64 = undefined;
//         try std.posix.getrandom(std.mem.asBytes(&seed));
//         break :blk seed;
//     });
//     const rng = prng.random();
//
//     var card_db = CardDb{ .cards = undefined };
//     try card_db.init(&allocator);
//     defer card_db.deinit(&allocator);
//
//     var deck = Deck{
//         .cards = card_db.cards,
//         .size = card_db.cards.len,
//     };
//
//     try deck.shuffle(&rng);
//
//     for (0..card_db.cards.len) |i| {
//         var count: u64 = 0;
//         for (0..card_db.cards.len) |j| {
//             if (i == j) continue;
//             if (card_db.cards[i].rank == card_db.cards[j].rank and card_db.cards[i].suite == card_db.cards[j].suite) count += 1;
//         }
//         try std.testing.expect(count == 0);
//     }
// }
1 Like

Thanks - so here’s your problem:

pub fn help() !void {
    try std.io.getStdOut().writer().print("HELP:\n", .{});
    for (&commands) |*cmd| {
        try std.io.getStdOut().writer().print("{s: 10<}{s}\n", .{ cmd.name, cmd.description });
    }
}

This line here:

try std.io.getStdOut().writer().print("{s: 10<}{s}\n", .{ cmd.name, cmd.description });

Has an unrecognized format on the first bit: {s: 10<} and I’m not quite sure what you’re trying to express there.

Here’s why it shows up when you uncomment that line - it’s lazy evaluation. In this bit here:

        for (commands) |command| {
            _ = command;
        }

The commands array is referenced for a first time. Once it’s referenced, the compiler evaluates its dependent code - the help function is one of those dependencies and that has a bug in it.

2 Likes

Oh, that was it. I wouldn’t have gotten there from the compiler error, thank you. It turns out I swapped the alignment and width format parameters, what I actually wanted was {s: <10}.

Where can I read more about how zig’s lazy evaluation works? The only thing I’m seeing in the docs from a quick search is a quick aside here - Documentation - The Zig Programming Language.

I wonder how feasible it would be to generate a compiler error from the line with the format string1instead of a completely unrelated line. In my case, zig build -freference-trace was calling out this line, which in hindsight is completely unrelated?

 _ = try stdin.readUntilDelimiter(&input_buffer, '\n');

I’m annoyed enough that I’m tempted to try and make a patch for this, but I’m also enough of a newbie that I’m probably way out ahead of myself here.

Regardless, thanks for your time, I appreciate the help.

I’d start here: Why Zig doesn't check for (semantic) errors in unused references?

If you decide to do a patch, I wish you luck - nothing wrong with trying :slight_smile: