Great question.
I also posted on using the methods method to see what specializations were happening or not happening. This is how I check myself to see if I understand what Julia is doing under-the-hood.
Here’s the version with no specialization:
julia> f(T::Type) = T
f (generic function with 1 method)
julia> methods(f).ms[1].specializations
svec()
julia> f(Int64)
Int64
julia> methods(f).ms[1].specializations
svec(#undef, #undef, #undef, #undef, #undef, #undef, #undef, MethodInstance for f(::Type{T} where T))
julia> f(Float64)
Float64
julia> methods(f).ms[1].specializations
svec(#undef, #undef, #undef, #undef, #undef, #undef, #undef, MethodInstance for f(::Type{T} where T))
Despite calling f with both Int64 and Float64 as arguments, only a single MethodInstance is needed to handle both arguments.
Here is the version with specialization:
julia> g(::Type{T}) where T = T
g (generic function with 1 method)
julia> methods(g).ms[1].specializations
svec()
julia> g(Int64)
Int64
julia> methods(g).ms[1].specializations
svec(MethodInstance for g(::Type{Int64}), #undef, #undef, #undef, #undef, #undef, #undef, #undef)
julia> g(Float64)
Float64
julia> methods(g).ms[1].specializations
svec(MethodInstance for g(::Type{Int64}), MethodInstance for g(::Type{Float64}), #undef, #undef, #undef, #undef, #undef, #undef)
Now we have a MethodInstance that is specialized for Int64 and another MethodInstance that is specialized for Float64.
methods returns a Base.MethodList with two fields, ms and mt. ms is just a Vector{Method}. mt is a Core.MethodTable. Both contain a wealth of information about the resulting methods.