semicolon changes the expression from returning the result of the expression to running the expression and returning the unit type. if you accidentally do that and specified a non-unit-return-type in the function signature, the type checker tells you about it:
error[E0308]: mismatched types
--> src/main.rs:1:14
|
1 | fn test() -> i32 {
| ---- ^^^ expected `i32`, found `()`
| |
| implicitly returns `()` as its body has no tail or `return` expression
2 | 5;
| - help: remove this semicolon to return this value
The rule is very simple and obvious, and the compiler will yell at you if you get it wrong.
It's also very useful and even critical of how expression-oriented the language is: an `if/else` or a match statement must typecheck, all branches have to have the same type. It's obvious if you're using it as an expression, but it doesn't go away if you're using it for the side-effect (as an imperative conditional/switch) and then things can get more dicey as the expressions in each branch can have different types. `;` solves that by making every branch `()`-valued.