Understanding decisions behind semicolons

I would like to see cases where that becomes a problem and the typeck error isn't absolutely clear about what happened (which I'm sure happens, I have an open PR that does a dry-by change of they style I'm thinking of). In my eyes the solution is to detect that and point at the last relevant semi and tell you remove it.

1 Like

That would entirely destroy my mental model of ; (which matches how ; is typically defined in functional languages, as was mentioned above): ; is a binary operator that discards its first operand, and returns the second. So I'm glad Rust doesn't work the way you suggest. It maks a lot mroe sense to me that a trailing ; just makes the expression return ().

Admittedly, the special rules around if and friends already dilute this mental model,, but it still works "well enough" for me. Special-casing the last statement in a block seems a lot more arbitrary.

Not entirely; OCaml uses let x = expr in ... where Rust uses let x = expr; .... FWIW I strongly prefer the Rust version nowadays, and wish I'd used it in the grammar of some of my academic languages that I can' reasonably change any more now. :wink:

This might seem like a small difference, but it's actually pretty significant -- in OCaml, e1; e2 is an expression, and there's no way e1 can introduce new binders to be used by e2. I don't think OCaml even has a notion of "statements" like Rust does; it is not needed.

3 Likes

Yeah, see, my mental model of ; comes from C, where it's not an operator, and all statements end with either a ; or a block. And my mental model of block values is Lisp's progn, which evaluates to the value of the last form within its arguments, and being Lisp there's no semicolon-or-no-semicolon distinction, there can't be. So to me, it's a weird rough edge in the language that { a; b; c } isn't a syntax error, and also a weird rough edge that { a; b; c; } evaluates to () rather than the value of c.

I'm not seriously suggesting that anything be changed, for the record, I just want to demonstrate that "Rust doesn't have enough semicolons" is just as grounded a reaction as "Rust has too many semicolons." It's all about what you're used to.

2 Likes

You can write C-style code in Rust though:

a;
b;
return c;

C doesn't really have anything to say for { a; b; c; } I'd say.

The lisp comparison seems apt though, I wasn't aware of that.

But anyway this is probably drifting into the wildly subjective. :wink:

This is 100% what I do (coming from a C/C++/Objective-C background).

Standard C doesn't, but clang and GCC have ({}) statement expressions where ({ a; b; c; }) evaluates to c.

Sometimes I'm a bit envious of you developers with functional programming experience (I've done some Racket, but only some) and formal education around language semantics. My mental model is... primitive... when it comes to ;.

1 Like

Maybe not complaints, but Go's semicolon insertion rules regularly confuse beginners and occasionally inconvenience more experienced developers:

2 Likes

{ return a, b, c; } :wink:

(Though it's not an expression like the GCC extension.)

It says that { a; b; c } is a syntax error.

Lua is a bit more interesting in how it works: It doesn't require semicolons between statements but also doesn't have significant newlines. Semicolons are only ever used to separate statements that could be interpreted either way.

This is valid Lua:

local a = 5 local b = a + 7

This example requires a semicolon to be interpreted as two statements.

local a = something ;
("string"):gsub("%l", "a")
1 Like

Honestly, I think optional semicolons that are required in some edge cases is the worst of both worlds. Now, instead of just using semicolons as a statement terminator (which helps improve diagnostics for malformed code as well!), you have to remember what the syntactic special cases where semicolons are required are. That's a significantly higher burden on the programmer, even if it's only the case for a; (b) (for any bracket type) and a; -b (for any prefix/infix operator), and no more.

You could say that this is fixable with a good automatic formatter, but the same logic applies in the other direction. If you forget a semicolon, the parser is actually really good at noticing and telling you exactly where to put them (with a machine applicable cargo fix), due to the exact reason that you could omit semicolons.

(This macro still working is absolute proof of that.)

15 Likes

With that macro, it even gives an error if i put a semicolon there: :rofl:

warning: unnecessary trailing semicolon
 --> src/main.rs:8:18
  |
8 |         let b = 5;
  |                  ^ help: remove this semicolon
  |
  = note: `#[warn(redundant_semicolons)]` on by default
1 Like

What sorcery is this... :no_mouth:

5 Likes

I would assume that that was rhetoric, but this intrigued me, so some links for anyone curious:

First a note on how it works; $s:stmt matches statements without their terminating semicolon. Hence the semicolons are summoned from the void by macro expansion. More properly, the macro sees let a = 2 as a valid statement, and ; as another. https://doc.rust-lang.org/reference/macros-by-example.html#metavariables

It is (at least was) intended that statements have to be followed by one of => , ; to prevent ambiguity now or in future versions, but this was never enforced for repeat matches. A macro rule ($a:stmt $b:stmt) => does get a compiler error.

Documentation including the intent for that requirement: https://doc.rust-lang.org/reference/macros-by-example.html#follow-set-ambiguity-restrictions

Formal specification including the note in bold explaining that the relevant rule is not currently enforced: https://doc.rust-lang.org/reference/macro-ambiguity.html#the-matcher-invariants

disclaimer: This is just what I could find in various resources, I'm no expert in any of this.

4 Likes

There used to be a future-compat lint for some cases when using this macro :no_mouth:

1 Like