crystal icon indicating copy to clipboard operation
crystal copied to clipboard

Function return type to be automatically typecasted similar to assignment/function args

Open vgramkris opened this issue 4 years ago • 6 comments

With Crystal:1.0.0 LLVM:10.0.0, Assignment is automatically typecasted c : Float64 = 0_i32

Function argument is automatically typecasted

def func1 ( a : Float64 )
  a
end
func1(0_i32)

But, return type casting is not done automatically. This causes compilation error - "Error: method top-level func2 must return Float64 but it is returning Int32"

def func2 : Float64
  # Return type as Float64 is not accepted; Compilation error for func2
  0
end

@asterite have clarified that the behavior is as expected with the current version. But it would be logical to have same rules applied for all these cases. While it is valid point that user would expect same type that is being enforced for the return type, then the same rule would be expected to be applied for assignment and function arg cases as well.

https://forum.crystal-lang.org/t/integer-return-type-not-typecasted-only-when-returning-from-function/3402

vgramkris avatar Jun 20 '21 03:06 vgramkris

Thinking a bit more about this, the main difficulty is code like this:

def foo : Float64
  if something
    1
  else
    another_call
  end
end

Here people will probably expect that 1 to mean a float, but it's not easily visible that this value is returned.

Well, it's doable, but I don't know how intuitive or understandable it will be.

asterite avatar Jun 20 '21 09:06 asterite

I agree that this requires a larger discussion and agreement. In the meantime, if we can note this specific use case's behavior and automatic type conversion rules/hierarchies in crystal lang documentation then that would be helpful for developers on what to expect.

vgramkris avatar Jun 20 '21 19:06 vgramkris

Do we think this would be simpler/easier for Enum based return restrictions? Mainly given that there's less ambiguity since it maps 1:1 to an enum member? If so that might be a quick win, then can save the numeric based contexts for later after more discussion.

Blacksmoke16 avatar Jun 23 '21 02:06 Blacksmoke16

No, there's not much of a difference between autocasting enum members and numbers.

Remember, an autocasted symbol literal :foo can refer to a FOO member of any enum. Picking the correct enum type based on the type restriction is pretty similar to picking the number type.

straight-shoota avatar Jun 23 '21 09:06 straight-shoota

Def bodies are just one of the contexts where a Crystal::MainVisitor is used; while working on this I realized we can generalize this to many other language constructs. We could do ProcLiteral bodies for example, using the explicit return type: (this was mentioned in #11218)

-> : Int64 { 1 } # okay

Complex variable assignments, using their explicit types:

x : Int64 = begin; 1; end # okay

Complex instance / class variable initializers, using their explicit types: (they are done a bit earlier than the main semantic phase)

class Foo
  @x : Int64 = begin; 1; end # okay
end

Block returns and nexts:

def foo(& : -> Int64)
  yield
end

foo { next 1 if ...; 2 } # okay

Breaks from blocks: (but see #11033)

def foo(& : ->) : Int64
  yield
  1_i64
end

foo { break 2 } # okay

Not all of these are equally intuitive, of course.

The general flow is the compiler needs to identify all the exit points of a given AST node before type binding occurs, otherwise it would be too late to intercept the compiler error. Then any AST node would have autocasting to some explicit type applied to its exit points, and plain literals become a base case (every NumberLiteral or SymbolLiteral must evaluate to itself). The exit point analysis would also be useful in e.g. #7707 where trailing TupleLiterals are generated for only the exiting MultiAssigns.

HertzDevil avatar Jan 10 '22 13:01 HertzDevil

Another example:

The following code doesn't work (from the forum)

class Person
  def self.instance : self
    @@instance ||= new 
  end
end
  
class John < Person
end

p! typeof(Person.instance)
p! typeof(John.instance) # Error: method John.instance must return John but it is returning Person+

In order to make it work we need to explicitly cast it:

class Person
  def self.instance : self
    (@@instance ||= new).as(self)
  end
end

I think of a returned type annotation as an implicit cast of the values it returns.

beta-ziliani avatar Sep 13 '22 13:09 beta-ziliani