Yes, when () is meant to yield a expression, you can put several statements
in it. For example, consider:
(x = Module.new; p x)::C = 1
the first segment in a constant reference like that is an expression.
Normally, that expression is a simple constant reference, as in A::C = 1,
or a variable, as in a::C = 1, in which case parentheses are not needed.
But if you use parentheses, the expression in turn can be composed of
multiple statements. As with return values in methods, the value of the
()-expression is the value of the last internal expression.
In the example above, we have a first expression, which is an assigment:
x = Module.new
Its value is discarded, but x now stores a value. After it, there is a
semicolon (could be a newline too), so Ruby continues execution and finds
p x
that method call prints the module object stored in x (say,
#<Module:0x0000000107787b40>), and returns x. Since x is what the last
expression evaluates to, that is what the whole () evaluates to, a module
object.
With that module object, Ruby moves forward and assigns C into said module.
Now, parentheses have different meanings in different places. For example,
if you do this
enum.each do |(a, b)|
...
end
(a, b) does not play the role of an expression, right? Similarly, in your
examples with puts,
puts(...)
is parsed as puts + the argument list of the call. Again, not an
expression. In an argument list, a semicolon does not make sense, it is a
syntax error.
The argument list itself is made of expressions, though, and therefore if
you double them
puts((...))
then what we saw above applies to the inner one.
Control expressions do not use parentheses in Ruby, so you can use (...) as
an expression there as well.