This package provides the @stable
macro
to enforce that functions have type stable return values.
using DispatchDoctor: @stable
@stable function relu(x)
if x > 0
return x
else
return 0.0
end
end
Calling this function will throw an error for any type instability:
julia> relu(1.0)
1.0
julia> relu(0)
ERROR: TypeInstabilityError: Instability detected in function `relu`
with arguments `(Int64,)`. Inferred to be `Union{Float64, Int64}`,
which is not a concrete type.
Code which is type stable should safely compile away the check:
julia> @stable f(x) = x;
with @code_llvm f(1)
:
define i64 @julia_f_12055(i64 signext %"x::Int64") #0 {
top:
ret i64 %"x::Int64"
}
Meaning there is zero overhead on this type stability check.
You can use @stable
on blocks of code,
including begin-end
blocks, module
, and anonymous functions.
The inverse of @stable
is @unstable
which turns it off:
@stable begin
f() = rand(Bool) ? 0 : 1.0
f(x) = x
module A
# Will apply to code inside modules:
g(; a, b) = a + b
# Will recursively apply to included files:
include("myfile.jl")
module B
# as well as nested submodules!
# `@unstable` inverts `@stable`:
using DispatchDoctor: @unstable
@unstable h() = rand(Bool) ? 0 : 1.0
# This can also apply to code blocks:
@unstable begin
h(x::Int) = rand(Bool) ? 0 : 1.0
# ^ And target specific methods
end
end
end
end
All methods in the block will be wrapped with the type stability check:
julia> f()
ERROR: TypeInstabilityError: Instability detected in function `f`.
Inferred to be `Union{Float64, Int64}`, which is not a concrete type.
(Tip: you cannot import or define macros within a begin...end
block, unless it is at the "top level" of a submodule. So, if you are wrapping the contents of a package, you should either import any macros outside of @stable begin...end
, or put them into a submodule.)
(Tip 2: in the REPL, you must wrap modules with @eval
, because the REPL has special handling of the module
keyword.)
You can disable stability errors for a single scope
with the allow_unstable
context:
julia> @stable f(x) = x > 0 ? x : 0.0
julia> allow_unstable() do
f(1)
end
1
although this will error if you try to use it simultaneously from two separate threads.
You can provide the following options to @stable
:
default_mode::String="error"
:- Change the default mode from
"error"
to"warn"
to only emit a warning, or"disable"
to disable type instability checks by default. - To locally or globally override the mode for a package that uses DispatchDoctor, you can use the
"instability_check"
key in your LocalPreferences.toml (typically configured with Preferences.jl).
- Change the default mode from
default_codegen_level::String="debug"
:- Set the code generation level to
"min"
to only generate a single function body for each stabilized function. The default,"debug"
, generates an entire duplicate function so that@code_warntype
can be used. - To locally or globally override the code generation level for a package that uses DispatchDoctor, you can use the
"instability_check_codegen_level"
key in your LocalPreferences.toml.
- Set the code generation level to
default_union_limit::Int=1
:- Sets the maximum elements in a union to be considered stable. The default is
1
, meaning that all unions are considered unstable. A value of2
would indicate thatUnion{Float32,Float64}
is considered stable, butUnion{Float16,Float32,Float64}
is not. - To locally or globally override the union limit for a package that uses DispatchDoctor, you can use the
"instability_check_union_limit"
key in your LocalPreferences.toml.
- Sets the maximum elements in a union to be considered stable. The default is
Each of these is denoted a default_
because you may set them globally or at a per-package level with Preferences.jl
(see below).
You might find it useful to only enable @stable
during unit-testing,
to have it check every function in a library, but not throw errors for
downstream users. You may also want to have warnings instead of errors.
For this, use the default_mode
keyword to set the default behavior:
module MyPackage
using DispatchDoctor
@stable default_mode="disable" begin
# Entire package code
end
end
"disable"
as the mode will turn @stable
into a no-op, so that
DispatchDoctor has no effect on your code by default.
The mode is configurable
via Preferences.jl,
meaning that, within your test/runtests.jl
, you could add a line before importing your package:
using Preferences: set_preferences!
set_preferences!("MyPackage", "instability_check" => "error")
You can also set to be "warn"
if you would just like warnings.
You might also find it useful to set
the default_codegen_level
parameter to "min"
instead of
the default "debug"
. This will result in no code duplication,
improving precompilation time (although @code_warntype
and error
messages will be less useful).
As with the default_mode
, you can configure the codegen level with Preferences.jl
by using the "instability_check_codegen_level"
key.
Note that for code coverage to work as expected over stabilized code,
you will also need to use default_codegen_level="min"
.
Note
There are several scenarios and special cases for which type instabilities will be ignored. These are discussed below.
- During precompilation.
- In unsupported Julia versions.
- When loading code changes with Revise.jl*.
- *Basically,
@stable
will attempt to travel through anyinclude
's. However, if you edit the included file and load the changes with Revise.jl, instability checks will get stripped (see Revise#634). The result will be that the@stable
will be ignored.
- *Basically,
- Within certain code blocks and function types:
- Within an
@unstable
block - Within a
@generated
block - Within any function containing a
@nospecialize
macro - Within a
quote ... end
block - Within a
macro ... end
block - Within an incompatible macro, such as
@eval
@assume_effects
@pure
- Or anything else registered as incompatible with
register_macro!
- Parameterized functions like
MyType{T}(args...) = ...
- Functions with an expression-based name like
(::MyType)(args...) = ...
- A function inside another function (a closure).
- But note the outer function will still be stabilized. So, e.g.,
@stable f(x) = map(xi -> xi^2, x)
would stabilizef
, but notxi -> xi^2
. Though ifxi -> xi^2
were unstable,f
would likely be as well, and it would get caught!
- But note the outer function will still be stabilized. So, e.g.,
- Within an
Note that you can safely use @stable
over all of these cases, and special cases will automatically be skipped. Although, if you use @stable
internally in some of these cases, like calling @stable
within a function on a closure, such as directly on the xi -> xi^2
, then it can still apply.
Say that you start using @stable
and you run into a type instability error.
What then? How should you fix it?
The first thing you can try is using @code_warntype
on the
function in question, which will highlight each individual variable's
type with a special color for any instabilities.
Note that some of the lines you will see are from DispatchDoctor's inserted
code. If those are bothersome, you can disable the checking with
Preferences.set_preferences!("MyPackage", "instability_check" => "disable")
followed by restarting Julia.
Other, much more powerful options to try include
Cthulhu.jl and
JET.jl, which can
provide more detailed type instability reports in an easier-to-read
format than @code_warntype
. Both packages can also descend into
your function calls to help you locate the source of the instability.
- Using
@stable
is likely to increase precompilation time. (To reduce this effect, try thedefault_codegen_level
above) - Using
@stable
over an entire package may result in flagging type instabilities on small functions that act as aliases and may otherwise be inlined by the Julia compiler. Try putting@unstable
on any suspected such functions if needed.
Many thanks to @chriselrod and @thofma for tips on this discourse thread.