Function return type to be automatically typecasted similar to assignment/function args
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
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.
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.
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.
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.
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.
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.