I was thinking about the Appendix with JET.jl of the Rust vs Julia in scientific computing blog, and I started wondering if we could create a code_errortype
or @code_errortype
that errors if @code_warntype
would display red. This would allow us to test for the presence of Any
when we did not intend to use dynamic typing.
In the appendix of the blog, a distinction is made between Rustβs type inference and Juliaβs type inference. This difference is due to the semantics of Rust being distinct from Julia in terms of when type inference can be done. In the example below, the type of v
is not determined until v.push(1.0)
.
Rust is allowed to retroactively determine the type of v
due to type inference for v
, but Julia is not. Vec::new()
does not imply any element type here.
In Julia, v = []
is equivalent to v = Any[]
. The Any
element type is implied, and the type of v
is determined on that line as Vector{Any}
. Itβs not that Julia cannot do type inference here, itβs just that we never allowed or asked it to do so while implicitly asking Julia to use dynamic typing.
We can ask Julia to do type inference at the construction of v1
though if this is done after at least one element types is known.
julia> function test()
v2 = [1.0] # Vector{Float64}, type inference
v1 = [v2] # Vector{Vector{Float64}}, type inference
push!(v1, [1]) # The type of v1 does not change!
# [1] is converted from Vector{Int} to Vector{Float64}
last = pop!(v1) # Vector{Float64}
println("OK") # OK
println(last.pop()) # JET.jl static analysis error
println("No problem!") # Unreachable
nothing
end
test (generic function with 1 method)
julia> @report_call test()
βββββ 1 possible error found βββββ
β test() @ Main ./REPL[68]:8
ββ getproperty(x::Vector{Float64}, f::Symbol) @ Base ./Base.jl:37
ββ type Array has no field pop: Base.getfield(x::Vector{Float64}, f::Symbol)
ββββββββββββββββββββββ
Not only does @code_warntype
know that v1
will be Vector{Vector{Float64}}
. It even figured out that the function is not going to be returning nothing
.
julia> @code_warntype test()
MethodInstance for test()
from test() @ Main REPL[68]:1
Arguments
#self#::Core.Const(test)
Locals
last::Vector{Float64}
v1::Vector{Vector{Float64}}
v2::Vector{Float64}
Body::Union{}
1 β (v2 = Base.vect(1.0))
β (v1 = Base.vect(v2))
β %3 = v1::Vector{Vector{Float64}}
β %4 = Base.vect(1)::Vector{Int64}
β Main.push!(%3, %4)
β (last = Main.pop!(v1))
β Main.println("OK")
β Base.getproperty(last, :pop)
β Core.Const(:((%8)()))
β Core.Const(:(Main.println(%9)))
β Core.Const(:(Main.println("No problem!")))
βββ Core.Const(:(return Main.nothing))
Compare that output of @code_warntype
to the following which knows the return type.
julia> function test_nopop()
v2 = [1.0] # Vector{Float64}, type inference
v1 = [v2] # Vector{Vector{Float64}}, type inference
push!(v1, [1]) # The type of v1 does not change!
last = pop!(v1) # Vector{Float64}
println("OK") # OK
#println(last.pop()) # Runtime error π₯
println("No problem!") # Not reachable
nothing
end
test_nopop (generic function with 1 method)
julia> @code_warntype test_nopop()
MethodInstance for test_nopop()
from test_nopop() @ Main REPL[91]:1
Arguments
#self#::Core.Const(test_nopop)
Locals
last::Vector{Float64}
v1::Vector{Vector{Float64}}
v2::Vector{Float64}
Body::Nothing
1 β (v2 = Base.vect(1.0))
β (v1 = Base.vect(v2))
β %3 = v1::Vector{Vector{Float64}}
β %4 = Base.vect(1)::Vector{Int64}
β Main.push!(%3, %4)
β (last = Main.pop!(v1))
β Main.println("OK")
β Main.println("No problem!")
βββ return Main.nothing
The appendix amounts to βJET.jl static analysis does not work when we ask Julia to be dynamicβ. Thatβs fair.
However, we also have tools to check for dynamic types and it may be possible to refactor to avoid them. In this specific case, after noting the differences between how Rust and Julia do type inference, we could actually refactor the Julia code to infer the type of v1
to be a Vector
with a narrower element type.
One capability that is missing is the ability to detect βredβ in @code_warntype
as part of testing.
julia> function code_errortype(f, types; optimize::Bool=false, debuginfo::Symbol=:default, kwargs...)
for (src, rettype) in code_typed(f, types; optimize, kwargs...)
p = src.parent
nargs::Int = 0
if p isa Core.MethodInstance
p.def isa Method && (nargs = p.def.nargs)
end
if src.slotnames !== nothing
slotnames = Base.sourceinfo_slotnames(src)[nargs+1:end]
non_dispatchable = (!Base.isdispatchelem).(src.slottypes[nargs+1:end])
if any(non_dispatchable)
msg_io = IOContext(IOBuffer(), :color => true)
println(msg_io, "Non-dispatchable slot type detected. Consider improving type inference:")
for (nd, name, type) in zip(non_dispatchable, slotnames, src.slottypes[nargs+1:end])
if nd
print(msg_io, " ", name)
Base.emphasize(msg_io, "::$type")
println(msg_io)
end
end
error(String(take!(msg_io.io)))
end
end
end
end
code_errortype (generic function with 1 method)
julia> function test()
v1 = [] # Vector{Any}
v2 = [1.0]
push!(v1, v2) # v1 is still Vector{Any}
last = pop!(v1) # Any
println("OK") # OK
println(last.pop()) # Runtime error π₯
println("No problem!") # Not reachable
nothing
end
test (generic function with 1 method)
julia> code_errortype(test, Tuple{})
ERROR: Non-dispatchable slot type detected. Consider improving type inference:
last::Any
Stacktrace:
[1] error(s::String)
@ Base .\error.jl:35
[2] code_errortype(f::Function, types::Type; optimize::Bool, debuginfo::Symbol, kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
@ Main .\REPL[256]:21
[3] code_errortype(f::Function, types::Type)
@ Main .\REPL[256]:1
[4] top-level scope
@ REPL[258]:1
Some questions for the reader:
- Does a function such as
code_errortype
already exist? - Do you have any suggestions how to improve this detection function?
- Should the presence of
Vector{Any}
also be reported as an issue? - Does a return type of
Union{}
indicate that the method always throws?