MilesCranmer / DispatchDoctor.jl

The dispatch doctor prescribes type stability
Apache License 2.0
128 stars 6 forks source link

Specialization of `Type{T}` within keyword arguments #27

Open MilesCranmer opened 1 month ago

MilesCranmer commented 1 month ago

The following behavior occurs:

julia> using DispatchDoctor

julia> @stable f(; t::Type{T}) where {T} = one(T)
f (generic function with 1 method)

julia> f(t=Float32)
ERROR: TypeInstabilityError: Instability detected in `f`
defined at REPL[2]:1 with keyword arguments
`@NamedTuple{t::DataType}` and parameters `(:T => Float32,)`.
Inferred to be `Any`, which is not a concrete type.

I think this may be "real" and in some ways a fundamental issue with how Julia handles keyword calls. Actually Test.@inferred does the same thing:

julia> f(; t::Type{T}) where {T} = T
f (generic function with 1 method)

julia> Test.@inferred f(t=Float32)
ERROR: return type Type{Float32} does not match inferred return type DataType

Keyword arguments are passed to Julia functions as a NamedTuple via Core.kwcall. However, we have that

julia> typeof((; t=Float32))
@NamedTuple{t::DataType}

and thus Julia relies on constant propagation for this to not be type unstable, it seems. Or at least Base.promote_op can't figure it out from the types alone. The only way this is type stable is if you rely constant propagation.

In other words, if your function's return type depends on a keyword argument which is itself a type (NOT a value, it must be a DataType!), then that function seems to be type unstable, and so should be rewritten. The only way it can be type stable is if you get constant propagation.

MilesCranmer commented 1 month ago

Note also you can get around this with a trick:

julia> f(; t::Val{T}) where {T} = T
f (generic function with 1 method)

julia> Test.@inferred f(t=Val(Float32))
Float32
MilesCranmer commented 4 weeks ago

Also posted an issue on Julia https://github.com/JuliaLang/julia/issues/54661