How is the term "expression" defined in Zig?

The Zig official documentation views declarations and assignments as expressions.

So can I think that everything in Zig are expressions?

I’m not a language lawyer, but from what I remember from previous discussions, function declarations are not expressions, which is why we can’t do:

const foo = fn(bar: u8) u8{ return bar; }

There was a proposal for making them expressions, which would allow this. I liked it, but it was rejected.
Also, the argument to @import and when writing extern "foo" have to be string literals, and I think the reason for that is that they are not expressions.
Other than that, I think almost everything else is an expression.

2 Likes

i’m not an expert, but i think the distinction is: does it end with a semicolon? if yes, it’s an expression, if no, it is not.

// fields are not expressions
a: i32,
// decls are expressions
pub const b: i32 = 5;
// but function definitions are not
pub fn foo() void {
    std.debug.print("{d} + {d}", .{b, b});
}
// granted, functions can be part of expressions
pub const bar = foo;
// loops and conditionals are not expressions
if (false) {
    @compileError("whoops!");
}
// but they can become part of such
pub const baz = if (false) @compileError("heyo") else 42;
1 Like

A notable exception compared to C is assignments. But apparently that will change (marked accepted last week)

5 Likes

I think there are some misunderstandings here. To illustrate this point, consider @TypeOf. In the documentation, its parameters are described as ‘expressions’.

1 Like

Since you believe I misunderstand (which may be true), I’d appreciate it if you would elaborate: I actually don’t see a conflict between what I wrote and the documentation for @TypeOf. In particular, I cannot put a function definition or a field inside @TypeOf without escaping to container scope, but I can put an if

I originally mistakenly thought you assumed the expression included a semicolon. However, even if you remove the semicolon, the declaration here still cannot be used as a parameter for @TypeOf. I think your definition is more suitable for describing a ‘statement’.

1 Like

gotcha. you’re right, I cannot put pub const b: i32 = 5 inside of @TypeOf. what would be the difference, in your view, between the words used to describe an if which requires a semicolon and one which does not?

An expression is a term one uses when talking about a language’s grammar. Generally speaking, it’s a language construct that you can put someplace where a value belongs in the language. This may be a variable name, a literal, a function call, and various other operations. Note that function calls and operations in particular are expressions that themselves accept sub-expressions. E.g. addition expression may be defined like <expression> + <expression>, and that whole thing also counts as a single big <expression>.

In a language’s grammar, there may be many different constructs, but expressions and statements are particularly notable. In C-like languages, the body of a function consists of a list of various statements.

In C, a statement may be a variable declaration, an if-else, a loop, a block, or even a self-contained expression ending with a semicolon, a return statement, break, continue, etc., etc., etc. Notably, statements are not themselves expressions, so in C you cannot put an if-else in an assignment – that’s why C has a separate ternary operator, which is an expression. An assignment in C is an expression, which means that you can legally put it in an if condition, which historically led to many bugs where people put = where they meant to put ==.

In Zig, the situation is quite a bit different. Many more more constructs are actually expressions on the grammar level, with some specific restrictions put in place as to where certain types of expressions are allowed to be (for example, you’re not allowed to put an assignment expression in an if condition in Zig). As opposed to C, return in Zig is an expression (with a sub-expression), so that you can actually put it after an orelse, or catch. A block is also an expression in Zig. I would argue that it is much less useful for a Zig programmer to know which constructs are expressions, because most simply are, and the border between an expression and a statement is quite blurry.

8 Likes
test "if without a semicolon" {
    try std.testing.expect(void == @TypeOf(if (false) {
        @compileError("whoops!");
    }));
}
const std = @import("std");

I’m not very good at describing syntax, but in Zig, perhaps a simple way to put it is if(condition) expression else expression, where each branch must be an expression. For statements that are not expressions, you can wrap them with {...}, which then becomes an expression of type void.

Regarding the issue of semicolons, I admit that what I said earlier was wrong: a semicolon is merely a syntactic component, and it is not necessarily related to a statement. It only has to appear at the end of certain statements, whereas in a block statement it does not need to appear at the end.

BlockStatement
    <- Statement
     / KEYWORD_defer BlockExprStatement
     / KEYWORD_errdefer Payload? BlockExprStatement
     / !ExprStatement (KEYWORD_comptime !BlockExpr)? VarAssignStatement

Statement
    <- ExprStatement
     / KEYWORD_suspend BlockExprStatement
     / !ExprStatement (KEYWORD_comptime !BlockExpr)? AssignExpr SEMICOLON

ExprStatement
    <- IfStatement
     / LabeledStatement
     / KEYWORD_nosuspend BlockExprStatement
     / KEYWORD_comptime BlockExpr

IfStatement
    <- IfPrefix BlockExpr ( KEYWORD_else Payload? Statement )?
     / IfPrefix !BlockExpr AssignExpr ( SEMICOLON / KEYWORD_else Payload? Statement )

The above content comes from the document. I think this part of the document is the most convincing in answering the OP’s question.

1 Like

It needs sometimes.

if (true) if (true) {};

Yes, because if (true) {} itself is considered an ordinary expression in if (true) if (true) {}, rather than a block expression.

2 Likes

I can understand what you mean. But it looks there is a self-contradictory‌ in the description. if (true) {}, an ordinary expression, should be followed by a semi-colon. :smiley:

Maybe a more precise description is “solo code blocks or the ones acting as the branch expressions of the outermost control flows …“. (sorry for my poor English).

1 Like

In this regard, we are exactly the same. :smiling_face_with_tear: Using natural language to describe it inevitably runs out of words, so the ‘grammar’ section in the appendix of the document is the best.

1 Like

Comparing with the explanations of Result Location Semantics, it looks the official documentation is self-contradictory‌ on the “expression” term definition.

By the narrow definition (expressions can be used as arguments of @TypeOf calls), now the following are not expressions:
* assignments (I’m glad they will become expressions.)
* declarations
* builtin functions
I’m surprised that functions with anytype parameters also have types, even if anytype itself is not a type.

fn foo(x: anytype) void {
    return x;
}

pub fn main() void {
    @import("std").debug.print("{}\n", .{ @TypeOf(foo) }); // fn (anytype) void
}

The function itself belongs to expressions(More precisely, it is identifier here); note the difference from a ‘function declaration’. Whether it is a generic function does not affect this point.

The grammar makes it a bit complicated. The reason both e.g. IfStatement and IfExpr exist is to allow for statement-level “block-like” expressions ({}, switch (x) {}, if (x) {} else {}, etc.) to be written without terminating semicolons, without allowing for both {} and {}; which would effectively make some semicolons optional. The distinct statement variants of these expressions make it a syntax error to leave a semicolon after a statement-level block (however there are currently some blind spots like the aforementioned if (true) if (true) {};).

Disregarding that complexity, you can kinda say that Zig has three classes of statements:

  • var/const declarations and assignments
  • defer/errdefer statements
  • statement-level expressions (which sometimes aren’t semicolon-terminated if they are block-like)

(Also note that assignment expressions are currently a special case, as pointed out earlier in the thread.)

3 Likes