I've always found the rules quite simple, once you think of everything as expressions (and not lines):
"." ends an expression in the shell.
In modules, "." ends forms. Forms are module attributes and function declarations. Forms are technically not expressions as they don't return anything. This is why they are terminated in a different manner than the rest.
Given forms =/= expressions, It could be argued that the shell's use of "." is what's not standard here.
"," separates expressions:
C = A+B, D = A+C
Note that 'if ... end', 'case ... of ... end', 'begin ... end', 'fun() ... end' and 'try ... catch ... end' are all considered expressions as they return a single value. As an example, it's possible to do
Var = case ... of ... end
to get a single value out of it. As such, you might see conditional expressions followed by ",".
";" Has two roles: separating different function clauses:
fac(0) -> 1;
fac(N) -> N * fac(N-1).
And separating different branches of expressions like 'if ... end', 'case ... of ... end' etc.:
if X < 0 -> neg;
X > 0 -> pos;
X == 0 -> zero
end
It's probably the most confusing one because the last branch doesn't need to have the ";" following it: it separates them, not ends them. As such, you might encounter code written the following way, which is arguably more readable:
if X < 0 -> neg
; X > 0 -> pos
; X == 0 -> zero
end
This makes things a bit more explicit. Because function clauses contain expressions, you might see a 'case' construct followed by ";", too (and similarly for nested constructs like the aforementioned 'try ... catch ... end' and others).
So as you can see, "." is for Forms, and ",;" are for separating language constructs/expressions. This is why You can see an if expression's 'end' done the three following ways: 'end.', 'end,' and 'end;'. Depending on where the expression is, it will be followed by different stuff.
You need to see it more like a template you can fill in than "here, let me put an end to this line!":
head1(Args) [Guard] ->
Expression1,Exp2,...,ExpN;
head2(Args) [Guard] ->
Expression1,Exp2,...,ExpN;
headN(Args) [Guard] ->
Expression1,Exp2,...,ExpN.
I don't see the rules as complex; they make sense, but you need to get used to them. If you think about it, things like 'for(int i = 0; i >= x; i++){ ... }' (or even 'for(...);') have a weird syntax when compared to most other constructs in languages supporting them. We're just so used to them we don't mind them anymore.