Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Statements and expressions

Eter is an expression-oriented language, meaning that most semantic constructs within the language evaluate to a value. To understand the syntactic and semantic structure of a program, it is fundamental to distinguish between statements and expressions:

  • Expressions are combinations of variables, literals, operators, and function calls that are evaluated by the compiler to compute and return a resulting value. Every expression has a well-defined type. Because they yield values, expressions can be arbitrarily nested and composed to build more complex logic.
  • Statements are instructions that dictate the control flow and state mutations of a program. They do not evaluate to a usable value (conceptually, they evaluate to the unit type unit). Their primary purpose is to execute side effects, such as introducing new bindings into a scope, modifying existing memory locations, or defining items.

In short: expressions compute values, while statements perform actions.

Expressions and statements are intrinsically linked: you can convert an expression into an expression statement by appending a semicolon ;. This forces the compiler to evaluate the expression solely for its side effects (e.g., executing a function call) while explicitly discarding any resulting value.

Statements

Statements are executed in sequence within a block. Eter supports several types of statements, including item declarations, assignments, and expression statements.

Statement terminator

Statements are generally terminated by a semicolon ;. The semicolon indicates the end of a statement and separates it from the next one.

Note

Some statements, such as those ending with a block expression (e.g., if, while, match), do not require a trailing semicolon unless they are part of a larger expression or assignment.

Assignment statements

An assignment statement evaluates an expression and binds its value to a memory location represented by a place expression (such as a variable, a struct field, or an array index).

The syntax for an assignment statement uses the = operator or a compound assignment operator (e.g., +=, -=, *=).

TypeExampleDescription
Basic Assignmentx = 5;Assigns the value 5 to the variable x.
Compound Assignmenty += 2;Adds 2 to y and assigns the result back to y.
Field Assignmentpoint.x = 10;Assigns 10 to the field x of point.

Assignment statements perform an action and do not evaluate to a value, meaning they cannot be chained like a = b = c.

Item declaration statements (let)

Item declaration statements introduce new items into the current scope. The most common item declaration within a block is the let statement, which binds a new local variable by always specifying its name and type. Variables are immutable by default unless marked with the mut keyword (more in the Memory Model Chapter).

DeclarationDescriptionExample
Variable BindingBinds a value to a new local variable, with a required type annotation.let name: str = "Alice";
Mutable BindingBinds a value to a mutable local variable.let mut count: i32 = 0;

Nested Items and Scoping

Items (such as nested let variables) can be declared inside any block scope. This allows you to restrict the visibility of an item strictly to the block it was declared in, keeping the outer namespace clean.

Nested items can be declared inside regular function blocks and unnamed (anonymous) scopes { ... } blocks. They are particularly useful inside functional blocks like if or if-else expressions to encapsulate temporary logic.

fn main() {
    let outer_val: i32 = 10;
    let condition: bool = true;

    // 1. Functional Block Scope (if-else)
    // Useful for isolating variables or helper logic
    // that is only needed for a specific branch.
    if condition {
        let inner_val: i32 = 20;
        let local_mul: i32 = 5;
        let result: i32 = inner_val * local_mul;
        do_something(result);
    } else {
        let fallback_val: i32 = 0;
        do_something(fallback_val);
    }
    // Error! `inner_val`, `local_mul`, and `fallback_val`
    // are out of scope and cannot be accessed here.
}

Expression statements

An expression statement is an expression that is evaluated for its side effects, followed by a semicolon. The value produced by the expression is discarded.

Common examples include function calls.

TypeExampleDescription
Function Calldo_something("Hello");Executes the function and discards the return value.

When a block-based expression (such as if, match, or a simple { ... } block) is used as an expression statement, the trailing semicolon is optional.


Expressions

An expression evaluates to a value and can be used in most places where a value is expected. Expressions can be nested and combined.

Literal expressions

A literal expression consists of a literal token and evaluates to the value represented by that token.

#![allow(unused)]
fn main() {
let age: i32 = 42;         // Integer literal
let name: str = "Alice";   // String literal
let is_valid: bool = true; // Boolean literal
let letter: char = 'A';    // Character literal
}

Path expressions (::)

A path expression refers to an item, variable, or constant in the current scope or another module using the path separator ::. You can also use paths to explicitly refer to the current module (self) or the parent module (super).

#![allow(unused)]
fn main() {
let max_val: u32 = u32::MAX; // Fully qualified path to a constant
let math_pi: f64 = math::PI;      // Path to a module item

// Accessing an item within the current actual scope
let current_item: i32 = self::helper_function();
}

Block expressions

A block expression is a sequence of statements enclosed in braces {}. The value of a block expression is the value of its final expression (the one without a trailing semicolon). If the block ends with a statement (with a semicolon), it evaluates to unit.

#![allow(unused)]
fn main() {
let y: i32 = {
    let x: i32 = 5;
    ret x + 1; 
}; // y is now 6
}

Operator expressions

Operator expressions apply unary or binary operators to operands.

#![allow(unused)]
fn main() {
let sum: i32 = 10 + 20;         // Binary arithmetic operator
let is_false: bool = !true;      // Unary logical NOT operator
let flag: bool = (a == b);       // Binary comparison operator
}

Grouped expressions

Parentheses () can be used to explicitly group expressions and control the order of evaluation, overriding default operator precedence.

#![allow(unused)]
fn main() {
let result: i32 = (2 + 3) * 4;  // Evaluates to 20 instead of 14
}

Access expressions

Access expressions allow you to retrieve specific elements from compound types like arrays, tuples, and structs. Depending on the underlying type, the syntax to access an element varies.

Array and index expressions

Array expressions create fixed-size collections of elements. Index expressions retrieve elements from an array or slice using brackets []. Indexing is always zero-based.

#![allow(unused)]
fn main() {
let a: [i32; 3] = [1, 2, 3];        // Array expression (list of elements)
let zeros: [i32; 5] = [0; 5];       // Array expression (repeated value: [0, 0, 0, 0, 0])
let first: i32 = a[0];              // Index expression (accessing the first element)
}

Tensor and index expressions

Tensor expressions create fixed-size, multi-dimensional collections of homogeneous elements (nD tensors). Tensor literal expressions use nested arrays.

#![allow(unused)]
fn main() {
let t: [i32; 2, 2] = [[1, 2], [3, 4]]; // Tensor expression (2x2 matrix)
let element: i32 = t[0][1];            // Index expression (accessing the element at row 0, column 1, which is 2)
}

Tuple and index expressions

Tuple expressions create ordered, fixed-size, heterogeneous collections. Tuple elements are accessed using dot . notation followed by a literal integer index.

#![allow(unused)]
fn main() {
let point: (i32, i32, str) = (10, 20, "label");  // Tuple expression
let x: i32 = point.0;                             // Tuple index expression (gets 10)
let desc: str = point.2;                         // Tuple index expression (gets "label")
}

Struct expressions and Field access

Struct expressions create instances of user-defined struct types. They specify the name of the struct and provide values for its fields. Field access expressions retrieve the value of a specific named field from a struct or union using the dot . operator.

#![allow(unused)]
fn main() {
// Struct instantiation expression
let p: Point = Point { x: 10, y: 20 }; 

// Field access expression
let my_x: i32 = p.x;                 // Accesses the 'x' field of the struct 'p'
}

Call expressions

Call expressions invoke functions or closures. They consist of an expression that evaluates to a callable entity, followed by a parenthesized list of arguments.

#![allow(unused)]
fn main() {
let result: i32 = add(5, 3);         // Function call
}

If expressions

An if expression evaluates a boolean condition and executes the corresponding block. If an else branch is provided, the if expression can evaluate to a value, provided both branches return the same type.

#![allow(unused)]
fn main() {
let condition: bool = true;

// if used as an expression to assign a value
let result: str = if condition {
    "Success"
} else {
    "Failure"
};

// if used for side effects (evaluates to `unit`)
if result == "Success" {
    do_something("Everything is fine");
}
}

Loop expressions

Loop expressions are used to execute a block of code multiple times. Because they are expressions, they can optionally evaluate to a value (e.g., by using the break keyword).

Eter supports both for and while loops.

#### While loops

The simple while loop continues to execute as long as its condition is true. Consider the following example, which decrements a variable until it reaches zero:

#![allow(unused)]
fn main() {
// while loop
let mut n: i32 = 5;
while n > 0 {
    n -= 1;
}
}

Infinite loops can be created using while true or the loop keyword, which will continue indefinitely until explicitly broken out of. For example:

#![allow(unused)]
fn main() {
// infinite loop using `while`
while true {
    do_something();
}
}

Since they are expressions, while loops can also be used to compute a value by using the break statement to exit the loop and return a value. For example:

#![allow(unused)]
fn main() {
// loop expression that evaluates to a value using `break`
let mut counter: i32 = 0;
let final_value: i32 = while true {
    counter += 1;
    if counter == 10 {
        break counter * 2; // The value of the loop expression will be 20 when it breaks
    }
};
}

For loops

Warning

Future development may introduce loop constructs, such as for in loops for iterating over collections. For instance, a for in loop might look like this:

#![allow(unused)]
fn main() {
let numbers: [i32; 5] = [1, 2, 3, 4, 5];
for num in numbers {
    do_something(num);
}
}

Match expressions

A match expression provides pattern matching. It compares a value against a series of patterns and executes the block corresponding to the first matching pattern. Like if expressions, match blocks can evaluate to a value if all branches resolve to the same type.

Patterns can include literals, variables, and the catch-all wildcard _.

Crucially, because each branch in a match must evaluate as an expression to return a value, you generally do not use a semicolon to terminate the branch’s expression. Instead, you use a comma , to separate and define the “next” pattern in the sequence.

#![allow(unused)]
fn main() {
let status_code: i32 = 404;

let message: str = match status_code {
    200 => "OK", // Notice the comma instead of a semicolon
    404 => "Not Found",
    500 => "Internal Server Error",
    // The underscore `_` acts as a wildcard, catching any unhandled values
    _ => "Unknown Error",
};
}

You can also use patterns to destructure types or bind variables:

#![allow(unused)]
fn main() {
let coords: (i32, i32) = (0, 10);

match coords {
    (0, 0) => do_something("Origin"),
    (x, 0) => do_something("On the X axis"),
    (0, y) => do_something("On the Y axis"),
    (x, y) => do_something("Somewhere else"),
}
}

Return expressions

A ret expression immediately terminates the current function or closure and evaluates to a value that is passed back to the caller. A ret expression without a value implies returning unit.

#![allow(unused)]
fn main() {
fn get_positive(val: i32) -> i32 {
    if val < 0 {
        ret 0; // Early return expression
    }
    
    ret val;
}
}

The implicit return in Eter is allowed only when there is a single expression within a scope (e.g., a function body or a block). If there are multiple statements, an explicit ret is required to indicate the return value. For instance, in the following function, the implicit return is not allowed because there are multiple statements:

#![allow(unused)]
fn main() {
fn compute_value(x: i32) -> i32 {
    let intermediate: i32 = x * 2; // Statement (requires a semicolon)
    ret intermediate + 1;           // Explicit return expression
}
}

While in this function, the implicit return is allowed because there is only a single expression:

#![allow(unused)]
fn main() {
fn compute_value(x: i32) -> i32 {
    x * 2 + 1 // Single expression (no semicolon, implicit return)
}
}

Note that, this extends to block expressions as well. If a block contains multiple statements, an explicit ret is required to return a value from that block. However, if the block consists of a single expression, it can implicitly return its value without needing ret.

#![allow(unused)]
fn main() {
let result: i32 = {
    let intermediate: i32 = 5; // Statement (requires a semicolon)
    ret intermediate + 10;     // Explicit return expression
};
let simple_result: i32 = {
    5 + 10 // Single expression (no semicolon, implicit return)
};
}

This feature is vital for writing concise and readable code, especially in cases where match and if expressions are used to compute values based on conditions. For example, in a match expression, with out the implicit return, you would need to write:

#![allow(unused)]
fn main() {
let status_code: i32 = 200;
let message: str = match status_code {
    200 => { ret "OK"; }, // Explicit return with `ret`
    404 => { ret "Not Found"; },
    500 => { ret "Internal Server Error"; },
    _ => { ret "Unknown Error"; },
};
}

Underscore expressions

The underscore _ can be used as an expression pattern to explicitly discard a value. This is useful when calling a function for its side effects, but explicitly acknowledging that you are choosing not to use its return value. It is also heavily used in match blocks as a wildcard (as shown above).

#![allow(unused)]
fn main() {
// Discard the result of a function that returns a value
let _: i32 = compute_heavy_task();
}