crystal-lang / crystal

The Crystal Programming Language
https://crystal-lang.org
Apache License 2.0
19.26k stars 1.61k forks source link

Require semicolon or newline before method body #13386

Open straight-shoota opened 1 year ago

straight-shoota commented 1 year ago

The compiler currently does not require a clear delimiter between the head and body of a method.

For example, def foo : Int32 1 end is valid syntax.

There are some oddities: def foo 1 end is not valid, but def foo() 1 end is.

Some typos can cause surprising behaviour. The following code is valid:

def foo : Array (String)
  [1]
end

The return type restriction is Array. (String) is parsed as the first expression in the method body.

A variation of that appears in #6191:

def foo(x : String) : Tuple (String, String) # Syntax error: unterminated parenthesized expression
  {x, x}
end

I think requiring an explicit delimiter could improve the last examples because they would lead to (meaningful) syntax errors.

This comment form @FnControlOption in https://github.com/crystal-lang/crystal/pull/11854#issuecomment-1518168910 makes a concrete suggestion:

def f(x) puts x end is both valid Ruby and Crystal, and requiring a semicolon in this case seems unnecessarily restrictive. I think a semicolon/newline should only be required if the method header (a) has both no parameters and no parentheses; or (b) has anything after the parentheses (i.e. an explicit return type and/or any generic type parameters).

I'm not sure about (a). A def without parenthesis cannot have parameters, so this case should be pretty unambiguous.

FnControlOption commented 1 year ago

I'm not sure about (a). A def without parenthesis cannot have parameters, so this case should be pretty unambiguous.

I agree—at least for Crystal—but it should be noted that this is already enforced in the parser, most likely to mirror Ruby.

For context, it's possible in Ruby to define a parenthesis-less, parameterized method. For example, the following is valid:

def f x
  puts x
end

In Crystal, however, this would raise a syntax error because all method headers must have parentheses around parameters.

In both languages, def foo bar end raises a syntax error. In Ruby, it's impossible to determine whether bar is a parameter to foo (def foo(bar) end) or if foo is parameterless and calls some bar method defined elsewhere (def foo() bar end). In Crystal, bar cannot be a parameter since it isn't enclosed by parentheses. However, this wouldn't be immediately obvious to a Rubyist who is new to Crystal.

straight-shoota commented 1 year ago

Yeah, it's probably better to be strict about parameterless defs, even though it's technically unambiguous in Crystal.

straight-shoota commented 1 year ago

Actually, the formatter already takes care of removing the potentially ambiguous syntax.

The first example from the OP formats like this:

def foo : Array
  (String)
  [1]
end

So the alternative is definitely considered ill-formed. But it's actually unambiguous and maybe it's fine to keep accepting it as valid code? If you want your code to be formatted in a sane way, you should use the formatter anyway =)

stefandd commented 2 months ago

I believe this is related!

def foo : Int32
    switch = true
    if switch 10 else 0 end # syntax error: Error: unexpected token: "else"
end
puts foo

The above doesn't compile but with an inserted ; it prints 10.

def foo : Int32
    switch = true
    if switch ; 10 else 0 end # works
end
puts foo
ysbaddaden commented 2 months ago

@stefandd switch 10 is interpreted as a function call with args, and else appears out of nowhere (syntax error). Adding the semicolon tells crystal to not look for args, and it parses correctly.