Hedronite Lesson · Polyglot-Dev / Rust · Mon 2026-08-31

Common Programming Concepts mutability types functions — the statement that is not an expression

A semicolon turns a value into silence. The book calls that a statement. Keep the semicolon off when you mean the value.

Lesson Class: Duha (Rust language track)
Focus: mutability/shadow/const · scalar/compound · statements vs expressions · if/loop/while/for
Code Blocks: clean blocks, explanation in prose
Done-criteria: pick mutability · types · functions · control flow from the book
Grounding: TRPL stable Ch.3 HTML ch03-00..05 · Blandy not cited
The bind
Immutable by default. mut rewrites. const always immutable. Shadow rebinds and can change type.
The return
Function bodies end in an expression. A semicolon turns that expression into a statement that returns ().
The branch
if is an expression with bool conditions and matching arm types. loop, while, and for cover repetition.
A semicolon turns a value into silence. The book calls that a statement. Keep the semicolon off when you mean the value.

<!-- hal:authoritative:yaml -->

A semicolon turns a value into silence. The book calls that a statement. Keep the semicolon off when you mean the value.

§I — Frame

Duha session 03. Third fire of the TRPL spine. The chapter is Common Programming Concepts. Four syllabus picks: mutability, types, functions, control flow. Comments get one short beat. They are not a fifth pillar.

Session 02 left you inside guessing_game. That crate still exists. Do not start a second game today. The book opens small crates named variables, functions, branches, and loops so each idea can compile alone. Use those names if you type along. The prior crate is background, not the work.

Coin the name now: the statement that is not an expression.

Statements perform an action and do not return a value. Expressions evaluate to a value. Put a semicolon after an expression and you convert it into a statement. The function then returns () instead of the i32 you promised. That error is the chapter's hinge. Hold it while you walk the rest.

Done-criteria from the syllabus: you can pick mutability, types, functions, and control flow from the book, not from guesswork.

§II — Immutable by default, then mut, const, shadow

Variables are immutable by default. Bind once. Reassign and the compiler stops you.

fn main() {
    let x = 5;
    println!("The value of x is: {x}");
    x = 6;
    println!("The value of x is: {x}");
}

cargo run yields error[E0384]: cannot assign twice to immutable variable \x\`. The help line offers let mut x = 5;. Add mut`. The second assignment compiles. Prints show 5, then 6.

Immutability is the default nudge. mut is the opt-out that tells the next reader the value will change. Prefer the default until a write is required.

Constants are always immutable. You never write mut on a const. Use const, annotate the type, and set only a constant expression. Naming convention: all uppercase with underscores.

const THREE_HOURS_IN_SECONDS: u32 = 60 * 60 * 3;

The compiler evaluates that product at compile time. You get one place to change the value later. Scope can be global.

Shadowing reuses a name with a fresh let. The second binding hides the first until the scope ends.

fn main() {
    let x = 5;
    let x = x + 1;
    {
        let x = x * 2;
        println!("The value of x in the inner scope is: {x}");
    }
    println!("The value of x is: {x}");
}

Inner scope prints 12. Outer prints 6 after the block ends. Shadowing is not mut. If you reassign without let, you get a compile error. With let again you can also change the type:

let spaces = "   ";
let spaces = spaces.len();

mut cannot do that type change. let mut spaces = " "; spaces = spaces.len(); is error[E0308]: expected &str, found usize. Shadow when the name stays and the type moves. Use mut when the same type must be rewritten in place.

§III — Scalar and compound

Rust is statically typed. Types are known at compile time. Inference usually fills them. When many types remain possible, annotate. Ch.2 already forced this on parse:

let guess: u32 = "42".parse().expect("Not a number!");

Leave the annotation off and you get error[E0284]: type annotations needed.

Two subsets: scalar and compound.

Scalar types hold one value. Four primaries: integers, floating-point numbers, Booleans, characters.

Integers: signed i8 through i128 plus isize; unsigned u8 through u128 plus usize. Default integer is i32. usize/isize follow the architecture width. Signed stores two's complement. Overflow in debug panics. In --release it wraps. Prefer the explicit methods (wrapping_*, checked_*, overflowing_*, saturating_*) when overflow is a real case.

Floats: f32 and f64. Default is f64. IEEE-754.

let x = 2.0; // f64
let y: f32 = 3.0; // f32

Booleans: true and false, type bool, one byte. Characters: char, four bytes, a Unicode scalar value, written in single quotes ('z', '😻'). Strings use double quotes. Do not treat char as "one grapheme a human sees." Chapter 8 owns that distinction.

Compound types group values. Two primitives: tuples and arrays.

A tuple has fixed length and may mix types. Destructure with a pattern, or index with .0, .1, .2.

let tup: (i32, f64, u8) = (500, 6.4, 1);
let (x, y, z) = tup;
let five_hundred = tup.0;

The empty tuple () is unit. Expressions that return nothing return unit.

An array has fixed length and one element type. Written [1, 2, 3, 4, 5]. Stack-allocated. Length is part of the type: [i32; 5]. Out-of-bounds index panics at runtime. Prefer a vector from the standard library when the length must grow. Chapter 8 covers vectors. Today the fixed array is enough.

Numeric operators are expressions bound by let: addition, subtraction, multiplication, division, remainder. Integer division truncates toward zero. -5 / 3 is -1 in the book's listing. Type suffixes on literals (57u8) and _ separators (1_000) are legal. Byte literal b'A' is u8 only.

Array type annotation names element and length together: let a: [i32; 5] = [1, 2, 3, 4, 5];. Same-value shorthand: let a = [3; 5]; is five threes. Index with a[0]. Wrong index panics. That panic is why for over the array is safer than a hand-rolled while index in Listing 3-4.

§IV — Functions, and the statement that is not an expression

Snake case for function and variable names. fn declares. Parameters need type annotations. Return type follows ->.

fn another_function(x: i32) {
    println!("The value of x is: {x}");
}

fn print_labeled_measurement(value: i32, unit_label: char) {
    println!("The measurement is: {value}{unit_label}");
}

Call order inside main is the execution order. Definition order in the file does not matter as long as the caller can see the name.

Function bodies are statements, optionally ending in an expression. Here is the coin in full:

let y = 6; is a statement. You cannot write let x = (let y = 6);. The compiler says expected expression, found \let\ statement. Assignment in C or Ruby returns a value. In Rust it does not.

A block is an expression when its last line has no semicolon:

fn main() {
    let y = {
        let x = 3;
        x + 1
    };
    println!("The value of y is: {y}");
}

y binds to 4. Add a semicolon after x + 1 and the block returns (). That is the statement that is not an expression: a value you meant to keep, silenced by ;.

Return values follow the same rule. The last expression is the return. Most functions omit the return keyword.

fn five() -> i32 {
    5
}

fn plus_one(x: i32) -> i32 {
    x + 1
}

Put a semicolon on x + 1 and the signature lies:

error[E0308]: mismatched types
 --> src/main.rs:7:24
  |
7 | fn plus_one(x: i32) -> i32 {
  |                        ^^^ expected `i32`, found `()`

The help line says remove the semicolon. Read that help as the chapter's punchline. You promised an i32. You filed a statement. Unit arrived instead.

§V — Comments, one beat

// starts a line comment. The compiler ignores it. Put the comment on its own line above the code more often than trailing the same line. Documentation comments are Chapter 14. Today // is enough.

§VI — Control flow: if as expression, then three loops

if is an expression. The condition must be bool. An integer is not a Boolean. if number { ... } with number: i32 is expected \bool\, found integer. Write if number != 0 when that is the test.

Arms are blocks. else if chains until the first true arm runs. Later arms are not checked. Too many else if arms point toward match in Chapter 6.

Because if is an expression, it can sit on the right of let:

let condition = true;
let number = if condition { 5 } else { 6 };

Both arms must produce the same type. if condition { 5 } else { "six" } is error[E0308]: \if\ and \else\ have incompatible types. The variable needs one type known at compile time.

Three loops: loop, while, for.

loop runs until break. You already used it in the guessing game. A break can carry a value out of the loop:

let mut counter = 0;
let result = loop {
    counter += 1;
    if counter == 10 {
        break counter * 2;
    }
};

result is 20. continue skips to the next iteration. Nested loops can take a label that starts with ', then break 'counting_up exits the labeled loop rather than the innermost one.

while keeps running while a condition is true. Listing 3-3 countdown:

let mut number = 3;
while number != 0 {
    println!("{number}!");
    number -= 1;
}
println!("LIFTOFF!!!");

for walks a collection without a manual index. Prefer it over while plus an index into an array. The index form can panic if the bound is wrong. The for form cannot overshoot the collection.

let a = [10, 20, 30, 40, 50];
for element in a {
    println!("the value is: {element}");
}

Countdown with a range and rev:

for number in (1..4).rev() {
    println!("{number}!");
}
println!("LIFTOFF!!!");

1..4 is exclusive on the end. That is the book's range for three ticks.

If you must nest loops, label the outer one. 'counting_up: loop { ... break 'counting_up; } exits the outer loop. A bare break exits only the innermost. return leaves the whole function. The guessing game used unlabeled break on a win; keep that memory as the simple case.

§VII — Four proofs

The syllabus names four picks. Run them from the book.

Proof 1. Mutability.

Default let refuses reassignment (E0384). let mut allows it. const is always immutable and needs a type. Shadowing uses a second let and can change type; mut cannot. If you explain mutability only as "variables can change," you missed the default and the shadow.

Proof 2. Types.

Name scalar versus compound. Defaults: i32, f64. bool for conditions. char in single quotes. Tuple destructure or .0. Array fixed length, same element type. Annotation required when parse leaves the type open. If you invent a growable array without naming a vector as later, you left Ch.3.

Proof 3. Functions.

Parameters annotated. Return type after ->. Body ends in an expression when you mean a value. A trailing semicolon on that expression returns (). Calling a function is an expression. Defining one is a statement. If plus_one returns () after you added ;, you found the statement that is not an expression.

Proof 4. Control flow.

if needs bool. if on the right of let needs matching arm types. loop / break / continue, optional label, optional value after break. while for a condition. for for a collection or a range. If you wrote if number without a comparison, or mixed 5 and "six" in one if expression, walk §VI again.

§VIII — Closing

Ch.3 is the vocabulary every later chapter assumes. Immutable by default. Types named and grouped. Functions that return the last expression. Control flow that is itself an expression when you need a value.

Name it when a semicolon erases the return you wrote: the statement that is not an expression. Remove the semicolon when the value must leave the block.

Session 04 is TRPL Ch.4, Ownership. What moves. What copies. Why a value has one owner. Do not start it in this folder. Do not write it today.

Examine well. The default bind is the first proof. The scalar and the compound are the second. The bare expression at the end of plus_one is the third. The if that binds, and the for that walks, are the fourth. The door is the missing semicolon.

Related

🛡️ ⚖️ 📜 Leo.Syri — Praetor Consulate, Imperium Luminaura Filed 2026-08-31 · Duha · session 03 · TRPL Ch.3 · the statement that is not an expression