The Semicolon Syntax Riddle

This compiles (if we declare std).

for (0..12)|i| for (i + 1..12)|j| for (j + 1..12)|k|
    std.debug.print("{},{},{}\n", .{i,j,k});

This however not. It requires an extra ; after the closing brace.

for (0..12)|i| for (i + 1..12)|j| for (j + 1..12)|k| {
    std.debug.print("{},{},{}\n", .{i,j,k}); 
} // ; needed

If we use braces for every loop (of course) not.

   for (0..12)|i| {
        for (i + 1..12)|j| {
            for (j + 1..12)|k| {
                std.debug.print("{},{},{}\n", .{i,j,k}); 
            }
        }
    }

What is the idea behind it?
I think it is syntactically very strange.

2 Likes

Because the inner loops become expressions which need to be terminated with ;.

an easier to understand example would be

const foo = for (0..1) |n| {
    //...
} else ...; // did ya know you could have an else on a loop :3

everything after the = becomes an expression.

10 Likes

this is due to how for statements are defined in the zig grammar. There’s actually 2 variants of for, ForStatement and ForExpr, one used when it’s present as a regular statement inside a statement block, the other one where an expression is expected.
The first for loop with the (0..12)|i| parameters is a ForStatement, which can have the form for (...)|...| {} or for (...)|...| <expression>; (ignoring for ... else ... here for simplicity)
The inner for loops are ForExpr in the cases where their parent for loop is the <expression> version, even if these nested ForExpr instances have a {} body, so you still need the terminating semicolon on the top-level brace-less ForStatement

9 Likes

might be related Allow any statement in loop bodies · Issue #5731 · ziglang/zig · GitHub

1 Like

No!

Thanks all. Study to do :slight_smile:

#1755 aims to reduce this kind of surprise.

5 Likes

Quite confusing actually…
Is there a situation thinkable in which }; is really really needed?
In my simple mind a closing brace is closing whatever there is inside.

const x: i32 = switch (x) {...} // why put a ; here?

I think in C# it is about the same.

Yes, it makes perfect sense to me when I

	const foo: Bar = init: {
		// ...
		break :init bar;
	}; // here I really expect the ; - I'm making an assignment

I meant “needed”. Not “making sense”. It makes sense to me in this case too

const x: i32 = switch(x) {...}
- 1; // ambiguous

With optional semicolons the compiler could either assume a semicolon is present, and throw error: value of type 'comptime_int' ignored for the - 1 on the line after, or assume a semicolon is not present, and compile without issues. With a required semicolon there’s no ambiguity, the - 1; is part of the initializer expression of x.

1 Like

This is a few years old but I think the language is unchanged in this regard: