Type inference for return types?

The Julia compiler/dispatcher rightfully complains about the following code because of the type annotations:

julia> foo_typed(f::Number)=f+1
foo_typed (generic function with 1 method)

julia> @code_llvm foo_typed("bar")
ERROR: no unique matching method found for the specified argument types

julia> foo_typed("bar")
ERROR: MethodError: no method matching foo_typed(::String)

However, when the return type is annoted there does not seem to be any type inference going on:

julia> foo()::Number="foo"
foo (generic function with 2 methods)

julia> @code_llvm foo()
;  @ REPL[59]:1 within `foo`
; Function Attrs: noreturn
define nonnull {}* @julia_foo_462() #0 {
top:
  %0 = alloca [2 x {}*], align 8
  %.sub = getelementptr inbounds [2 x {}*], [2 x {}*]* %0, i64 0, i64 0
  store {}* inttoptr (i64 140265051841024 to {}*), {}** %.sub, align 8
  %1 = getelementptr inbounds [2 x {}*], [2 x {}*]* %0, i64 0, i64 1
  store {}* inttoptr (i64 140265208134864 to {}*), {}** %1, align 8
  %2 = call nonnull {}* @jl_apply_generic({}* inttoptr (i64 140265107780880 to {}*), {}** nonnull %.sub, i32 2)
  call void @llvm.trap()
  unreachable
}

julia> Base.return_types(foo)
1-element Vector{Any}:
 Union{}

julia> foo()
ERROR: MethodError: Cannot `convert` an object of type String to an object of type Number

Is this a limitation of the compiler or the type system? It seems to me that union types make life a lot more complicated.

What did you expect? The type annotation is trying to convert a string to a Number, which of course will error. Union{} as a return type just means that the compiler could proove that the function will never return normally.

1 Like

I expected the compiler to throw an error when I declared the function.

That’s not how Julia works. Type inference is purely an optimization and will never change the behavior of your program. If an error is thrown inside the compiler, something is going horribly wrong.

3 Likes

Outside of method signatures it makes more sense to think of the :: operator as an assertion, i.e. you might want to make sure a method throws an exception rather than returning the wrong type.

2 Likes

I prefer to think of it as a compile-time decision to add a conversion step, on top of it being a type assertion.

f(x)::Float64 = x
g(x)::Number = x
h(x)::AbstractString = x

f(1) # 1.0 because of conversion step to Float64
g(1) # 1 because Int64 <: Number already, no conversion step needed
h(1) # errors because Int64 is not an AbstractString

@code_warntype h(1) will show you that the function was inferred to always throw an error via its return type being Union{}. The @code_warntype printout for f(1) and g(1) look the same, but if you dig further into the compilation process with @code_llvm, you can see that f(1) has a conversion step but the compiler figured out that g(1) didn’t need one. Remember, a specialization (version) of a method is compiled given the input concrete types, so the g(::Int64) specialization didn’t need a conversion step because Int64 is already a Number.

1 Like