While loop that always executes at least once (do-while loop)?

How would you refactor this to always run the contents of the while loop at least once?

pub fn readMailboxInTimeout(
    port: *Port,
    io: std.Io,
    station_address: u16,
    recv_timeout: std.Io.Duration,
    mbx_in: HalfConfiguration,
    mailbox_timeout: std.Io.Duration,
) ReadMailboxInTimeoutError!InContent {
    assert(mbx_in.isValid());

    const deadline = (try std.Io.Clock.Timestamp.now(io, .boot)).addDuration(.{ .clock = .boot, .raw = mailbox_timeout });
    while ((try std.Io.Clock.Timestamp.now(io, .boot)).compare(.lt, deadline)) {
        if (try readMailboxIn(
            port,
            io,
            station_address,
            recv_timeout,
            mbx_in,
        )) |in_content| {
            return in_content;
        }
    } else {
        return error.MailboxTimeout;
    }
}

I’d probably do it like this:

const deadline = ...;
while (true) {
    if (try readMailboxIn(...)) |in_content| return in_content;
    if (try ...now(...).compare(.gte,deadline)) return error.MailboxTimeout;
}

Maybe there’s something fancier you can do with Io (go-like select?) to return the first of two async tasks (either readMailboxIn or sleep(mailbox_timeout), whichever finishes first), but if I’m only allowed to change this one function then ^that’s what I’d do.

Here is your version

pub fn readMailboxInTimeout(
    port: *Port,
    io: std.Io,
    station_address: u16,
    recv_timeout: std.Io.Duration,
    mbx_in: HalfConfiguration,
    mailbox_timeout: std.Io.Duration,
) ReadMailboxInTimeoutError!InContent {
    assert(mbx_in.isValid());

    const deadline = (try std.Io.Clock.Timestamp.now(io, .boot)).addDuration(.{ .clock = .boot, .raw = mailbox_timeout });
    while (true) {
        if (try readMailboxIn(
            port,
            io,
            station_address,
            recv_timeout,
            mbx_in,
        )) |in_content| {
            return in_content;
        }
        if ((try std.Io.Clock.Timestamp.now(io, .boot)).compare(.gt, deadline)) return error.MailboxTimeout;
    }
}

Something that makes me uneasy about this code is the potential for me to somehow introduce a bug in the future that would cause this to never return. The while (true) is scary.

1 Like

Comparing with deadline looks perfect for while loop condition.
i.e.

const deadline = ...;
var once = false;
while (!once or (try std.Io.Clock.Timestamp.now(io, .boot)).compare(.le, deadline)) {
    once = true;
    if (try readMailboxIn(
        port,
        io,
        station_address,
        recv_timeout,
        mbx_in,
    )) |in_content| {
        return in_content;
    }
}
return error.MailboxTimeout;

EDIT: added once condition

2 Likes

This issue is that a short deadline my cause a return before readMailboxIn can execute at least once. I would like the semantics of this function to always execute readMailboxIn at least once (in the absence of errors).

I edited the code to add an once variable.

I previously discussed the usage implemented with blocks, and @Sze provided a solution based on inline functions that makes fuller use of zls hints:

pub inline fn do(work: void, @"while": bool) bool {
    _ = work;
    return @"while";
}

pub fn readMailboxInTimeout(
    port: *Port,
    io: std.Io,
    station_address: u16,
    recv_timeout: std.Io.Duration,
    mbx_in: HalfConfiguration,
    mailbox_timeout: std.Io.Duration,
) ReadMailboxInTimeoutError!InContent {
    assert(mbx_in.isValid());

    const deadline = (try std.Io.Clock.Timestamp.now(io, .boot)).addDuration(.{ .clock = .boot, .raw = mailbox_timeout });
    while (do({
        if (try readMailboxIn(
            port,
            io,
            station_address,
            recv_timeout,
            mbx_in,
        )) |in_content| {
            return in_content;
        }
    }, (try std.Io.Clock.Timestamp.now(io, .boot)).compare(.lt, deadline))) {}
    return error.MailboxTimeout;
}
4 Likes

Don’t be afraid of while (true).

andy@bark ~/s/zig (std.Io-fs)> grep -RI 'while (true)' lib/std/  | wc -l
484

Almost 500 instances in the standard library alone.

Put a break just before the end curly if it makes you feel better:

while (true) {




    break;
}
6 Likes

Hahaha yes, lots of while (true) in std. Sometimes I miss the old Pascal / Delphi repeat until

I’ve introduced max_iter vars before when a while (true) loop looks too sketchy.

2 Likes

FWIW I sometimes use a ā€˜running’ variable for this, e.g. smth like:

var running = true;
while (running) {
    // on exit condition set running to false
}

…sometimes that’s better than various breaks in the middle of the while-body when you want to make sure that the entire while body is executed even on the last iteration.

I actually miss do-while a bit :slight_smile: What I do miss in C’s do-while is that the lifetime of variables declared inside the while-body are not extended into the while-condition-check (e.g. sort of a reverse for (int i…);

2 Likes

The best way to replicate do-while loops is with continue expressions.

while (true) : (if ((try std.Io.Clock.Timestamp.now(io, .boot)).compare(.ge, deadline)) break) {
    if (try readMailboxIn(
        port,
        io,
        station_address,
        recv_timeout,
        mbx_in,
    )) |in_content| {
        return in_content;
    }
}
return error.MailboxTimeout;

This method is better and more refactoring-proof than putting the conditional break at the bottom of the loop body, not only because it prevents the break from getting lost in the sauce but also because continue statements won’t inadvertently skip over it.

6 Likes

So like this?

const expect = std.testing.expect;

while (true) : (expect((try std.Io.Clock.Timestamp.now(io, .boot)).compare(.ge, deadline)) catch break) {
   // ...
}
1 Like

This is the final implementation I came up with, using while continue expressions.

Its really cool that I can comptime unreachable after the while loop, the compiler knows that I never break from that while loop, only return.

Its a little ugly but I fully expect it to be more ergonomic after std.Io is more fleshed out.

/// Poll mailbox until timeout.
/// Guaranteed to poll at least once.
pub fn readMailboxInTimeout(
    port: *Port,
    io: std.Io,
    station_address: u16,
    recv_timeout: std.Io.Duration,
    mbx_in: HalfConfiguration,
    mailbox_timeout: std.Io.Duration,
) ReadMailboxInTimeoutError!InContent {
    assert(mbx_in.isValid());

    const start = try std.Io.Clock.Timestamp.now(io, .boot);
    const deadline = start.addDuration(.{ .clock = .boot, .raw = mailbox_timeout });
    // do-while loop to ensure mailbox is read at least once
    while (true) : ({
        const now = try std.Io.Clock.Timestamp.now(io, .boot);
        if (now.compare(.gt, deadline)) return error.MailboxTimeout;
    }) {
        if (try readMailboxIn(
            port,
            io,
            station_address,
            recv_timeout,
            mbx_in,
        )) |in_content| {
            return in_content;
        }
    }
    comptime unreachable;
}
4 Likes

This is a good trick to know. However, in this case you don’t need it. The compiler will complain if the loop is able to finish without a return, and there is no return statement after the loop. That will cause void to be implicitly returned, and void is not the return type of the function.

3 Likes

While I’m in no position to be questioning Andrew’s advice, given that ā€œAll loops must have a fixed upper-boundā€ is #2 on NASAs list of rules for safety critical code, I think a healthy trepidation of while (true) is not a terrible thing. source

1 Like

Yeah, I think sometimes people underestimate how good a do-while-loop can express the intent of the programmer compared to workarounds.

I still dislike that certain languages don’t have it for that very reason (although that doesn’t mean that I always have a clue how to include it in all of them, like for example Python).

I normally either put the loop body into a separate body, or (if the code is short enough) just duplicate it (and create comments that tells the programmer that).

Neither is ideal (the first because it makes you jump around the code and because of parameter passing; the second because it not really obvious from reading the code).

1 Like

Some additional context, I’m semi-confident that I will be refactoring this later to use a *std.Io.Writer as an out parameter instead of returning InContent (passing about 1500 bytes on the the stack). In that case the return value will change to !void.

Only the paranoid survive!

2 Likes

They also say:

No function should be longer than what can be printed on a single sheet of
paper in a standard reference format with one line per statement and one line per
declaration. Typically, this means no more than about 60 lines of code per function.

I think this an extremely goofy suggestion that is backed up neither by evidence nor sound reasoning.

Zig compiler source code for example would be a lot more convoluted if it followed this suggestion.

I doubt even TigerBeetle follows this crazy advice.

Let’s take a peek! In 20 seconds I cracked open src/vsr/replica.zig and found the open function which is 277 lines long.

6 Likes

They do limit the lines per function: