# Comptime control flow inside runtime block

**URL:** <https://ziggit.dev/t/comptime-control-flow-inside-runtime-block/4962>\
**Category:** Explain\
**Tags:** language\
**Created:** [July 3, 2024, 1:34pm UTC](https://ziggit.dev/t/comptime-control-flow-inside-runtime-block/4962 "2024-07-03T13:34:42Z")\
**Posts on this page:** 8\
**Page:** 1

<div class="post-metadata">

**Author:** ![chung-leong](https://ziggit.dev/user_avatar/ziggit.dev/chung-leong/32/999_2.png) [@chung-leong](https://ziggit.dev/u/chung-leong)\
**Post date:** [July 3, 2024, 1:34pm UTC](https://ziggit.dev/t/comptime-control-flow-inside-runtime-block/4962/1 "2024-07-03T13:34:42Z")

</div>

The following code doesn’t compile:

```zig
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});
    }
}

```

```zig
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:

```zig
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});
        }
    }
}

```

```zig
1
2
3
5

```

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

---

<div class="post-metadata">

**Author:** ![mnemnion](https://ziggit.dev/user_avatar/ziggit.dev/mnemnion/32/2478_2.png) [@mnemnion](https://ziggit.dev/u/mnemnion)\
**Post date:** [July 3, 2024, 5:48pm UTC](https://ziggit.dev/t/comptime-control-flow-inside-runtime-block/4962/2 "2024-07-03T17:48:12Z")

</div>

Should it?

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

```zig
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:

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

```

This is consistent with the documentation for [inline for](https://ziglang.org/documentation/master/#toc-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.

---

<div class="post-metadata">

**Author:** ![IntegratedQuantum](https://ziggit.dev/user_avatar/ziggit.dev/integratedquantum/32/782_2.png) [@IntegratedQuantum](https://ziggit.dev/u/IntegratedQuantum)\
**Post date:** [July 3, 2024, 6:47pm UTC](https://ziggit.dev/t/comptime-control-flow-inside-runtime-block/4962/3 "2024-07-03T18:47:43Z")

</div>

> [@mnemnion](#):
>
> `inline for` unrolls a loop. You can’t continue an unrolled loop, but you can break from one of its blocks.

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

```zig
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?

---

<div class="post-metadata">

**Author:** ![mnemnion](https://ziggit.dev/user_avatar/ziggit.dev/mnemnion/32/2478_2.png) [@mnemnion](https://ziggit.dev/u/mnemnion)\
**Post date:** [July 3, 2024, 9:14pm UTC](https://ziggit.dev/t/comptime-control-flow-inside-runtime-block/4962/4 "2024-07-03T21:14:15Z")

</div>

It’s unrolling into something like this:

```zig
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.

---

<div class="post-metadata">

**Author:** ![mlugg](https://ziggit.dev/user_avatar/ziggit.dev/mlugg/32/437_2.png) [@mlugg](https://ziggit.dev/u/mlugg)\
**Post date:** [July 4, 2024, 7:51am UTC](https://ziggit.dev/t/comptime-control-flow-inside-runtime-block/4962/5 "2024-07-04T07:51:24Z")

</div>

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.

---

<div class="post-metadata">

**Author:** ![mnemnion](https://ziggit.dev/user_avatar/ziggit.dev/mnemnion/32/2478_2.png) [@mnemnion](https://ziggit.dev/u/mnemnion)\
**Post date:** [July 4, 2024, 2:10pm UTC](https://ziggit.dev/t/comptime-control-flow-inside-runtime-block/4962/6 "2024-07-04T14:10:50Z")

</div>

> [@mlugg](#):
>
> Hm, I honestly think it’d be reasonable for `continue` to work for `inline` loops.

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.

---

<div class="post-metadata">

**Author:** ![jeffective](https://ziggit.dev/user_avatar/ziggit.dev/jeffective/32/3116_2.png) [@jeffective](https://ziggit.dev/u/jeffective)\
**Post date:** [December 23, 2024, 12:24am UTC](https://ziggit.dev/t/comptime-control-flow-inside-runtime-block/4962/7 "2024-12-23T00:24:41Z")

</div>

This is the issue:

> <https://github.com/ziglang/zig/issues/9524>
>
> Currently, trying to compile something like
> \`\`\`zig
> pub fn main() void {
> c…onst Foo = struct{a: ?u8, b: ?u8, c: ?u8, d: ?u8};
> 
> var test\_foo = Foo{.a = 5, .b = 6, .c = null, .d = 9};
> inline for(std.meta.fields(Foo)) |field\_info| {
> if(@field(test\_foo, field\_info.name) == null) continue;
> doStuff(@field(test\_foo, field\_info.name));
> }
> }
> \`\`\`
> won't work because the \`continue\` is based on a runtime value. However, there's no reason not to allow this behavior at runtime by unwrapping it into something like
> \`\`\`c
> if(test\_foo.a == null) goto for\_b;
> doStuff(test\_foo.a);
> for\_b:
> if(test\_foo.b == null) goto for\_c;
> doStuff(test\_foo.b);
> //etc
> \`\`\`
> 
> Although I will admit that this example is contrived, I just wanted a simple example to illustrate my point. For more complicated control flow, it can greatly simplify code and make it more readable.

Just hit this when implementing a deserializer for message pack and I wanted the deserializer to “retry” for every field in a union until one succeeded:

```zig
fn decodeUnion(comptime T: type, fbs: *FBS) !T {
    const starting_position = try fbs.getPos();
    inline for (comptime std.meta.fields(T)) |union_field| {
        const res = decodeFbs(union_field.type, fbs) catch |err| switch (err) {
            error.Invalid, error.EndOfStream => {
                try fbs.seekTo(starting_position);
                continue;
            },
        };
        return res;
    } else {
        return error.Invalid;
    }
    unreachable;
}

```

Edit:

I got this monstrosity to work:

```zig
fn decodeUnion(comptime T: type, fbs: *FBS) !T {
    const starting_position = try fbs.getPos();
    const rval = rval: inline for (comptime std.meta.fields(T)) |union_field| {
        const res = decodeFbs(union_field.type, fbs) catch |err| switch (err) {
            error.Invalid, error.EndOfStream => |err2| blk: {
                try fbs.seekTo(starting_position);
                break :blk err2;
            },
        };
        if (res) |good_res| {
            break :rval @unionInit(T, union_field.name, good_res);
        } else |err| switch (err) {
            error.Invalid, error.EndOfStream => {},
        }
    } else {
        return error.Invalid;
    };
    return rval;
}

test "decode union" {
    const MyUnion = union(enum) {
        my_u8: u8,
        my_bool: bool,
    };

    try std.testing.expectEqual(MyUnion{ .my_bool = false }, try decode(MyUnion, &.{0xc2}));
    try std.testing.expectEqual(MyUnion{ .my_u8 = 0 }, try decode(MyUnion, &.{0x00}));
    try std.testing.expectError(error.Invalid, decode(MyUnion, &.{0xc4}));
}

```

---

<div class="post-metadata">

**Author:** ![00JCIV00](https://ziggit.dev/user_avatar/ziggit.dev/00jciv00/32/1069_2.png) [@00JCIV00](https://ziggit.dev/u/00JCIV00)\
**Post date:** [December 24, 2024, 8:40pm UTC](https://ziggit.dev/t/comptime-control-flow-inside-runtime-block/4962/8 "2024-12-24T20:40:38Z")

</div>

There’s a small “hack” you can use in these situations as well. You simply `break` from the block instead of continuing the loop. This will jump you straight to the next unrolled block of the `inline for` loop at runtime and works effectively as a continue.

Example from your original code:

```zig
fn decodeUnion(comptime T: type, fbs: *FBS) !T {
    const starting_position = try fbs.getPos();
    inline for (comptime std.meta.fields(T)) |union_field| inlineCont: {
        const res = decodeFbs(union_field.type, fbs) catch |err| switch (err) {
            error.Invalid, error.EndOfStream => {
                try fbs.seekTo(starting_position);
                break :inlineCont;
            },
        };
        return res;
    } else {
        return error.Invalid;
    }
    unreachable;
}

```
