Footgun in labelled switch

Today I changed the bytecode interpreter main loop of my toy language “moin" from a classic while true / switch to a labelled switch.

However, this introduced a bug.

Before, I had

while(true) {
    opcode = frame.readOpCode();
    switch(opcode) { … }
}

(sry for the formatting, doesn’t really work on my mobile)

Inside some of the switch prongs, I used opcode to decide between instructions with short operands (1 byte) and long operands (3 bytes).

Changing this construct to a labelled switch, basically means to add continue :vmloop frame.readOpCode() at the end of each switch prong.

However, the opcode variable which is the original switch expression is still there, but no longer valid inside the prongs, it still contains the very first opcode.

Maybe there shouldn’t be an expression at all in a labelled switch?

1 Like

So it’s a bit like what I had here?
https://codeberg.org/ziglang/zig/issues/31002

You can kickstart the vmloop with something like vmloop: switch(frame.readOpCode())

1 Like

Yes, ofc, that’s what I did after finding the reason for the bug.

BTW the labelled switch resulted in ~ 5% performance improvement.

2 Likes

That would make many idioms not work, e.g grep the Zig compiler code base for : switch (

Yeah I noticed similar improvements, until the opcode set grew (or the prongs did) and I had to revert back to a mix of loop and labelled switch. It’s hard to please the LLVM opt pipeline, so I gate vmloop changes with performance regression tests.

The foot gun, when converting an existing conventional while/switch to a labelled switch, for me, was this:

After performing the obvious changes, I also had to change occurrences of `opcode` in the switch prongs.

The compiler does not (can not) help here.

The expression in parens after the switch keyword is only for the start of the loop.

Probably I wouldn’t have stumbled over this if I had created the labelled switch from scratch instead of adapting existing code.

See the changes in moin/src/vm.zig here:

https://codeberg.org/tumirnix/exla/commit/7b8395fafd97e5da218b9be28f98547d3c0afa07#diff-fac90981ba8e2a049d86d785f6eb28e22c93248b

I had to use |op|in the prongs instead of opcode.

A quick, powerful, and basically language-agnostic[1] protip for this kind of refactor:

Rename the variable above the switch statement to e.g. opcode_ temporarily. But not with the help of an LSP, just manually at the declaration site. This turns all references to it into compiler errors, so you can dig through them one-by-one and fix them as needed, without worrying that you’ll miss an occurrence. Footgun eliminated by making the compiler help you. :slight_smile:

Compiling with zig build --watch --prominent-compile-errors makes this sort of thing a breeze.


  1. Except for languages that don’t require variable declarations before use, like Python or non-strict JavaScript. ↩︎

8 Likes

I use this technique quite often, but in this case I just wasn’t aware of the issue at all.

I understand the footgun, this already happened to me, but labelled switch are one of my favorite feature in Zig, it’s like manually encoding an FSM and it just feels right, so I get that it’s not easy to use, and probably harder to translate/upgrade but if you have solid tests, regardless of the potential performance benefit, I think it helps readability, and robustness.

This isn’t a foot gun, it’s just an annoying change due to not knowing that switch captures applied to things other than tagged unions.

It’s surprising that people just talked about irrelevant or tangential things to the point you had to figure it out yourself.

Not sure I understand your first sentence.

Anyway, to describe the issue I had:

My brain: I know how this works.

Me, ignoring my brain, reading it like

switch |opCode| { ... }

I wasn’t saying this is a Zig-specific foot gun. I guess I’d make the same mistake in other languages, because the conventional while/switch pattern works like this usually.