A (probably stupid) terminological question


I have a question about {expression,statement,operator} terms.

I took a look at this explanation and after

Though the function is now an expression the way it is called above, the whole of the code is still a statement

my mind was broken :tired_face:

So, what is exact meaning of the terms above (“statement”, “expression”, “operator”) in Zig?

For example, in, say

const below_zero: bool = if (a < 0) true else false;

where are statements, where are operators, where are expressions?


Haven’t read the linked artible, but from what I’ve previously gathered:

  • Operators: = and <
  • Expressions: a, 0, true, false, and if (a < 0) true else false
  • Statement: const below_zero: bool = if (a < 0) true else false

Or in other words, expressions produce a value, statements don’t. Operators perform actions on operands. That’s pretry much it.

1 Like
  • is ‘if (a < 0) true else false’ statement or expression?
  • is ‘if (a < 0) true else false;’ statement or expression?

Or, in other words… :dotted_line_face:-

  • does ‘if (a < 0) true else false’ produce a value?
  • does ‘if (a < 0) true else false;’ produce a value?

if (a < 0) true else false is an expression, it returns a boolean. if (a < 0) true else false; is an invalid statement in zig since it is an expression that returns a value and values cannot be implicitly discarded. To make it valid statement, it can be changed to explicitly discard the value: _ = if (a < 0) true else false;. A statement requires some sort of operation to occur, often an assignment.

In addition to what @desttinghim writes, the flexibility that Zig’s if allows can make this topic even more confusing. In Zig, if you want to use if as an expression, you cannot use the curly braces around the branches and the else branch is mandatory:

const below_zero = if (a < 0) true else false; // OK

const below_zero = if (a < 0)
    false; // OK

const below_zero = if (a < 0) { true } else {false }; // error
const below_zero = if (a < 0) true; // error
const below_zero = if (a < 0) { true }; // error

const below_zero = if (a < 0) true else 13; // error, must produce same type

An if with the curly braces around the branches is not an expression, it’s a statement. Like function bodies, there’s no semicolon after the closing brace. The else branch is optional.

if (a < 0) {
    print("Below zero!\n", .{});

if (a < 0) {
    print("Below zero!\n", .{});
} else {
    print("Above zero!\n", .{});

What can be a little confusing is when you mix in labeled blocks, which you can use in an if expression, making it look like an if statement with curly braces:

const below_zero = if (a < 0) blk: {
    break :blk true;
} else blk: {
    break blk: false;

But even so, the labels are pretty easy to spot, enough to differentiate this from an if statement.


What is if by itself? An (unary) operator? Or what?

Yes, it has a value.

const std = @import("std");

pub fn main() void {
    const t: i32 = -7;
    if (if (t < 0) true else false)
        std.debug.print("<0\n", .{});

but if (t < 0) true else false; does not have a value

const std = @import("std");

pub fn main() void {
    const t: i32 = -7;
    if (if (t < 0) true else false;)
        std.debug.print("<0\n", .{});
$ /opt/zig-0.11/zig run if.zig 
if.zig:6:35: error: expected ')', found ';'
    if (if (t < 0) true else false;)

It does not have a value, but we can assign this nonexistent value to a var. :slight_smile:
Ok, we are assigning the value of if (t < 0) true else false.
Does that mean that ';' is a terminator for '='?!?!?

if by itself is just a keyword. It only becomes an expression or a statement depending on how you combine it with other expressions or statements.


pub by itself is also just a keyword.
and by itself is also just a keyword, but it’s also a (binary) operator.
pub is not an operator.

The ; terminates a statement, and in Zig, an assignment using the = operator is a statement. Note that in other languages, = is an expression like in:

a = b = c;

But this has led to bugs when you mistakenly use = when you meant ==. So most new langueges don’t treat = assignment as an expression any more. The same has happened with the ++ and -- operators. In ancient (lol) languages, these are expressions:

a = c++;
a = ++c;

But once again, this has led to much confusion and bugs when the programmer is not clear on what the actual value being returned is. In Go, they have postfix ++ and -- only and they are not expressions to avoid this confusion. In zig, they’re simply not included at all and you have to use += or -= which are not expressions either.

So back to if, I guess it’s hard to put it in a single category in Zig, given it can serve as an expression and a statement. The same is true for while, for, and switch which all have expression and statement forms.

So, statement is something terminated by ';', right?

In case of comparing with constants there is simple way to avoid such mistakes, just make the constant lhs:

if (NULL == ptr)

if by itself is neither expression nor statement.

I know. :slight_smile:
= by itself is not an expression, it’s a an operator.
with some lhs and ; somewhere below it becomes a statement, right?

Right. So to put it in the most fundamental way, in Zig all statements must end with ; and expresiions by themselves are not statements, so that’s why your example code if (a < 0) true else false; is not valid Zig code (as @desttinghim pointed out above.)

There are languages that allow expression statements like a + b;, but not Zig. In Zig the value returned from an expression must be used and cannot be ignored. Even discarding it with _ = a + b; counts as using the value.

oh, yeah! - statement expressions

“expression statements”, “statement expressions”
Curiouser and curiouser! :slight_smile:

1 Like

Are there any examples of “statements”, that do not require an assignment?

I think they are very rare indeed. Not 100% sure, but I think these are statements in Zig:

  • defer foo.deinit();
  • errdefer foo.deinit();
  • for (0..3) |i| { print("{}", .{i}) }
  • while (i < 3) |i| : (i += 1) { print("{}", .{i}); }
  • switch (a) { 0 => print("0", .{}), else => |x| print("{}", .{x}) }
  • if (a < 0) { print("<", .{}); } else { print(">=", .{}); }

explicit goto was better imho

Calling a void function is a statement, too, right? And that’s pretty common.

  • foo.update();
  • try foo.update();

This can be one of two options:

  1. A function always returns a value, and in the case of void, that’s the only value the language allows you to ignore.
  2. A function with return type void actually doesn’t return any value.

Given that in Zig there is an actual noreturn return type, I was inclined to believe option 1 was the case. Also, when return is followed by something, it has to be the value to return, so it does seem that a call to a void function returns a void value given this:

fn voidFunc1() void {}

fn voidFunc2() void {
    return voidFunc1();

A term missing from this discussion is “side-effect”. In general, statements have side-effects, meaning they cause changes to the environment, whereas expressions do not. The expression a + b doesn’t change anything. The statement c = a + b; does. Whatever stored in c previously is lost forever, replaced by the outcome of the addition expression.

Assignment to variables is the most common side-effect. There’re also operations like printing to the screen or outputting to a network connection.

Expressions can have side-effects, but that’s something we try to avoid since they make a program harder to understand. Obviously, programming with the use of variables is not very practical. We do try our best to isolate the side-effects, either within a function scope or within an object.

1 Like