Type inference when types are passed as values

I got a little confused about how type inference works, when trying to find out what happens when types are passed as values to a function like in rand(Int64). So far I thought that the compiler tries to infer the returned type of a function only using typeof(arg) for every argument arg. However, to me it seems that that then e.g. code_warntype(rand, (typeof(Int64), )) and @code_warntype rand(Int64) would have to be equivalent, while actually they give different results (the following is in v0.6.2 but the same happens in the 0.7 alpha):

julia> @code_warntype rand(Int64)
Variables:
  #self# <optimized out>
  T <optimized out>

Body:
  begin 
      $(Expr(:inbounds, false))
      # meta: location random.jl rand 358
      SSAValue(0) = $(Expr(:invoke, MethodInstance for rand(::MersenneTwister, ::Type{UInt64}), :(Base.Random.rand), :(Base.Random.GLOBAL_RNG), :(Base.Random.UInt64)))
      # meta: pop location
      $(Expr(:inbounds, :pop))
      return (Base.bitcast)(Int64, SSAValue(0))
  end::Int64

where the type is inferred correctly and

julia> code_warntype(rand, (typeof(Int64), ))
Variables:
  #self# <optimized out>
  T::DataType

Body:
  begin 
      return (Base.Random.rand)(Base.Random.GLOBAL_RNG, T::DataType)::Any
  end::Any

where it is not. This doesn’t seem to fit the documentation of @code_warntype, which states, that it “Evaluates the arguments to the function or macro call, determines their types, and calls code_warntype on the resulting expression.”.

I didn’t find any questions on discourse about this. In the documentation I found out about the singleton types Type{T} and that you can specialize methods on type values with that (which apparently is the case for rand) but I don’t see how this fits into inference.

Also, as code_warntype is the only way I know to get information about type inference, I’m not sure whether this is a question about code_warntype or about type inference. I think my confusion boils down to the following two questions though:

  1. Is it wrong to say that the compiler only uses information in typeof.(args) during inference?
  2. When checking for type stability, should I trust @code_warntype or code_warntype()?
1 Like

Great question. Yes, types are very special — their typeof is DataType, but they’re also subtypes of the very special Type, which can be parameterized to match one particular type. So, yes, it is wrong to say the compiler only uses typeof — it uses Core.Typeof.

As far as its trust-worthiness, we do try to keep it as accurate as possible. There are indeed some gotchas, though, particularly when it comes to functions that inline into their caller. For example, on 0.7:

julia> g(x) = x ? 1 : "one"
g (generic function with 1 method)

julia> @code_warntype g(true)
Body::Union{Int64, String}
1 1 ─     goto 3 if not %%x                                                                                                                                                                                                      │
  2 ─     return 1                                                                                                                                                                                                               │
  3 ─     return "one"                                                                                                                                                                                                           │

This function g is type unstable! But watch what happens when you call it from another function with a constant:

julia> f() = g(true)
f (generic function with 1 method)

julia> @code_warntype f()
Body::Int64
1 1 ─     return 1                                                                                                                                                                                                               │

Julia was able to infer g when called by a constant value — even though g itself isn’t type stable!

8 Likes

As for how this is implemented, it looks like @code_warntype (and other macros) use the unexported Base.typesof(args) instead of typeof.(args) to do this, https://github.com/JuliaLang/julia/blob/795626d0018a5d3d13c471ca81409fd0d021d3f7/stdlib/InteractiveUtils/src/macros.jl (gen_call_with_extracted_types function, called from the macros here, https://github.com/JuliaLang/julia/blob/795626d0018a5d3d13c471ca81409fd0d021d3f7/stdlib/InteractiveUtils/src/macros.jl#L70).

Edit: ah, Base.typesof calls Core.Typeof on master, as @mbauman mentioned.

1 Like

Thanks for the quick answer! Is there a specific reason, why there are two different "typeof"s?