Confused about returning a value (example in the book)

I'm new to Rust, and I'm going through the book, I'm at chapter 3.5 - returning values from the loops, where it says:

Then we declare a variable named result to hold the value returned from the loop.

Here's a bit of code that confuses me:
break counter * 2;

That's because in previous chapter

it says:

Note the x + 1 line without a semicolon at the end, which is unlike most of the lines you’ve seen so far. Expressions do not include ending semicolons. If you add a semicolon to the end of an expression, you turn it into a statement, which will then not return a value. Keep this in mind as you explore function return values and expressions next.

So my question is:
Why is that break counter * 2; returns a value? It works and compiles with or without a semicolon, by the way.

It returns a value because you say it to do so. In the same way return a; works as well.
It's just nice to leave return out at the end of a function. So
fn a() -> String {return "hey".to_string();}
is the same as
fn a() -> String {"hey".to_string()}. So basically the compiler searches for return statements or expressions (without semicolon) for the return value

So why does this example from 3.3 Functions:

    let y = {
        let x = 3;
        x + 1

returns a value, but this:

    let y = {
        let x = 3;
        x + 1;

Returns an empty tuple?

No, the return in your two code snippets will exit the current function/closure

This is because the last expression in the block, x + 1 is the value that the block evaluates to. In the second there is no value to be returned (kind of like void in languages like Java or C++), an no value is spelled () or unit in Rust. This is because you specifically asked for that behavior by putting the semicolon.

In Rust semicolons are semantically significant, not just some bit of syntax.

ahh yeah you're right

Yes I know, that's why I'm confused.
Why in the loop example both break counter * 2; and break counter * 2 work the same way? (return a value).

Ok, break evaluates to !, the never type. This is a speical type that can never have a value. Because it can never have a value, it is allowed to coerce to any other type. In this case it will coerce to (). So putting a semicolon after the break doesn't actually change anything.

break jumps out of the loop and forces the loop to evaluate to the value following the break, which is counter * 2, not counter * 2;. This difference is due to precedence rules.

So, the following two loops are the same.

let x = loop { break 0 };
assert!(x, 0);

let y = loop { break 0; };
assert!(y, 0);

If you want to look into this deeper, they're called "assignment expressions" in the Rust Reference. The relevant section makes an important point:

[An assignment expression] always has the unit type.

So to look at the example from @adamszymanowski again - what's assigned is to y in the first example is (), aka unit. In the second example, it's the result of the assignment.

In the first example it would by x + 1 or 4. In the second example it would be ().

@adamszymanowski Here is my mental model for all this:

  1. Statements are instructions to the computer, like sentences. The main example is let, as in let x = 1;.
  2. Expressions are things that have a value. Some examples
    x + 1
    if true { 4 } else { 6 }
    // assignments evaluate to `()`
    x = 1
    // semicolons are especially magic: when added to the end of an
    // expression they ignore its value and change it to `()`. They also allow you
    // to add an extra statement/expression afterwords, and this becomes
    // the main value of the block. This is the main way that
    // rust allows you to do non-functional things
        let mut x = 2;
        x = 3;
    // some statements evaluate to `!`, a special type that can never exist, and 
    // is therefore allowed to pretend to be any other type
    break 2
    return 4
    // so you can do cool things like
    let b = match x {
        0 => true,
        1 | 2 => false,
        _ => return 3,
  3. So hopefully you can see from these examples that your break statement is an expression, but the compiler knows that the expression is never evaluated, since the break statement interrupts normal control flow, so it really doesn't care what the value of the expression is.

NB This is the same as what the other answers are saying, but I find that hearing the same thing in different ways sometimes helps you to understand it better.

Okay, thanks for the replies, I guess it makes sense now.