crystal-lang / crystal

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

Pseudo-method inconsistencies #13432

Open devnote-dev opened 1 year ago

devnote-dev commented 1 year ago

I believe that the way pseudo-methods are parsed should be changed to be more fault-tolerant for the sake of consistency with the language. The following example fails to compile with the error message unexpected token: bar:

foo : Int32? = nil
bar = 123

if foo.is_a? bar
  puts foo + bar
end

This fails to compile with the error message: Error: unexpected token: "DELIMITER_START" (expected symbol)

foo : String? = nil

if foo.responds_to? "to_i"
  puts foo.to_i
end

And this compiles and runs fine despite being invalid syntax:

foo : Int32? = nil

if foo.as? Nil Int32
  puts true
end

if foo.nil? Nil
  puts true
end

Pseudo-methods are explicitly treated differently by the lexer and parser, when really they should be methods with additional restrictions, almost like reserved methods (which isn't exactly a new concept in Crystal anyway).

HertzDevil commented 1 year ago

Some pseudo-methods like is_a? accept type names that are not part of regular code (e.g. postfix *), so they are not "just" methods with additional restrictions.

devnote-dev commented 1 year ago

Assuming you mean things like UInt8*, I would have thought that resolves to Pointer(UInt8) in most cases, but my point about the restrictions was primarily the inability to redefine/overload them.

HertzDevil commented 1 year ago

Yes it resolves to ::Pointer(UInt8), but normal method or macro calls do not have this capability at all, because syntactically they never expect type names in their argument lists. This is also why some other things like offsetof cannot be turned into regular methods

straight-shoota commented 1 year ago

The main point is that there are some lax syntax rules around pseudo-methods which should be more restrictive. Also the error messages could be improved.

How to implement that is a different story. Removing some of the special case handling for pseudo-methods could be an option. We should look into that. But it's also clear that they are parsed explicitly for a reason because they have special syntactical features.

devnote-dev commented 1 year ago

.is_a? seems quite straightforward in the sense that the error message can just be updated to something friendlier. For .as/.as? and .nil?, they allow one or more tokens preceding the method call which would normally raise an invalid arguments count/signature exception at compile time. Is the parser ignoring these tokens, or is something else happening here?

HertzDevil commented 1 year ago
if foo.as? Nil Int32
  puts true
end

is equivalent to:

if foo.as?(Nil)
  Int32
  puts true
end

Same goes for normal methods, actually:

class Foo
  def foo(x)
    true
  end
end

if Foo.new.foo Nil Int32
  puts true
end

# equivalent to:
if Foo.new.foo(Nil)
  Int32
  puts true
end
devnote-dev commented 1 year ago

It's also worth noting that .as/.as?, .is_a?, .nil? and .responds_to? all have this issue, but it is only when there are 2 tokens given – any more will raise the appropriate exception.

HertzDevil commented 1 year ago

That too happens for normal methods, since if Foo.new.foo Nil Int32 Bool becomes if Foo.new.foo(Nil); Int32 Bool and the final space isn't allowed

devnote-dev commented 1 year ago

@HertzDevil Sorry I didn't see your comment – why is that? Shouldn't a semicolon be expected here?

HertzDevil commented 1 year ago

No idea. For the record an uppercase name can be a method in Ruby, so it is parsed like a method there:

# okay
def Bar(x)
  1
end

# okay, equivalent to Foo.new.foo(Bar(String))
# where `Bar(String)` is a method call, not a generic
if Foo.new.foo Bar String
  puts true
end
straight-shoota commented 1 year ago

The issue about statement end after an if condition is similar to https://github.com/crystal-lang/crystal/issues/13386. I guess we can expand that to all control structures.

FnControlOption commented 1 year ago

The issue about statement end after an if condition is similar to #13386.

Is there a reason why Crystal does not support if foo then bar end syntax like Ruby? I tried finding related discussions but searching "if then" in issues doesn't help narrow it down. 😅

Blacksmoke16 commented 1 year ago

Wouldn't that just be bar if foo? Probably just for sake of keeping things simple? :shrug:.

straight-shoota commented 1 year ago

I suppose it's probably an unnecessary alternative syntax. if foo; bar end works. There's no need for then.

devnote-dev commented 1 year ago

I would be in favor of using if..then..else syntax in place of ternary operations, given that Crystal is meant to be read like English, and then is only used in case..in/case..when statements anyway. But that's probably for another issue.