Comptime control flow inside runtime block

The following code doesn’t compile:

const std = @import("std");

pub fn main() void {
    var value: i32 = 4;
    _ = &value;
    inline for (.{ 1, 2, 3, 4, 5 }) |number| {
        if (value == number) {
            continue;
        }
        std.debug.print("{d}\n", .{number});
    }
}
test.zig:8:13: error: comptime control flow inside runtime block
            continue;
            ^~~~~~~~
test.zig:7:19: note: runtime control flow here
        if (value == number) {

On the other hand, the following works:

const std = @import("std");

pub fn main() void {
    var value: i32 = 4;
    _ = &value;
    inline for (.{ 1, 2, 3, 4, 5 }) |number| {
        do: {
            if (value == number) {
                break :do;
            }
            std.debug.print("{d}\n", .{number});
        }
    }
}
1
2
3
5

Why doesn’t the compiler handle the first case when it’s clearly capable of doing so?

3 Likes

Should it?

inline for unrolls a loop. You can’t continue an unrolled loop, but you can break from one of its blocks.

var number = 1;
do: {
    if (value == number) {
        break :do;
    }
    std.debug.print("{d}\n", .{number});
}

number = 2;
do: {
    if (value == number) {
        break :do;
    }
    std.debug.print("{d}\n", .{number});
}

number = 3; // etc

Compared with:

var number = 1; 
if (value == number) continue;
 // there's nothing to continue

This is consistent with the documentation for inline for:

For loops can be inlined. This causes the loop to be unrolled, which allows the code to do some things which only work at compile time, such as use types as first class values.

I would rather the compiler be consistent about this semantics, rather than try to translate constructs which don’t work in an unrolled loop into constructs which do.

3 Likes

Curiously you can also break from the loop itself at runtime:

const std = @import("std");

pub fn main() void {
    var value: i32 = 4;
    _ = &value;
    inline for (.{ 1, 2, 3, 4, 5 }) |number| {
        if (value == number) {
            break;
        }
        std.debug.print("{d}\n", .{number});
    }
}

Why does a runtime break from an unrolled loop work, but not a runtime continue?

3 Likes

It’s unrolling into something like this:

fn runtimeBreak(value: i32) void {
    var number: i32 = 1;
    inline_for: {
        if (value == number) break :inline_for;
        std.debug.print("{d}\n", .{number});
        number = 2;
        if (value == number) break :inline_for;
        std.debug.print("{d}\n", .{number});
        number = 3;
        if (value == number) break :inline_for;
        std.debug.print("{d}\n", .{number});
        number = 4;
        if (value == number) break :inline_for;
        std.debug.print("{d}\n", .{number});
        number = 5;
        if (value == number) break :inline_for;
        std.debug.print("{d}\n", .{number});
    }
}

test "runtime break" {
    runtimeBreak(4);
}

Granted, this is a little pinch of magic.

I’d call it an affordance: the compiler could require that we put a label on the inline for block, and use the label, but instead it’s kind enough to let us use the break statement directly.

But this is different from continue, which only works in a loop. Which isn’t what an inline for compiles to.

I consider the compiler inferring a label to be different from it translating one control-flow primitive into another, but YMMV.

3 Likes

Hm, I honestly think it’d be reasonable for continue to work for inline loops. It’d be pretty damn easy to make work in the compiler, too. I’d be happy for someone to open a proposal.

9 Likes

My feelings on the subject aren’t strong ones, but I’m inclined to think that the reminder that inline for isn’t a loop, that it’s unrolled, might be worth preserving.

On the other hand, it does have the “wait why doesn’t this work?” quality, so there’s that. A proposal would be the right place to hash it all out.