JET.jl

JET employs Julia's type inference system to detect potential bugs and type instabilities.

Note

The latest release series, v0.12, supports full JET functionality on Julia v1.12 and v1.13 only.

The JET version that works with Julia v1.11 is the v0.9 series, but note that bug fixes and new features added in later series are not necessarily available there.

Warning

Please note that due to JET's tight integration with the Julia compiler, the results presented by JET can vary significantly depending on the version of Julia you are using. Additionally, the implementation of the Base module and standard libraries bundled with Julia can also affect the results.

Moreover, Julia's compiler plugin system is unstable and changes frequently. Each JET release therefore supports full functionality on only a limited set of Julia versions. JET may remain installable on newer Julia versions, but loads empty stubs by default when full functionality is unavailable.

Julia compatibility and versioning

JET distinguishes installation compatibility from functional compatibility.

  • The latest JET series keeps its Julia upper compat bound open. This allows packages with JET as a test dependency to instantiate on Julia pre-releases and nightly builds.
  • Full JET functionality is loaded only on explicitly supported Julia versions. On unsupported future Julia versions, JET loads empty stubs and its analysis APIs throw an explanatory error.
  • JET minor versions are bumped for ordinary semantic-versioning reasons and may also mark a new Julia compatibility generation. Patch releases normally stay within the same compatibility generation.
  • When support for a new Julia minor is released, older JET releases are capped in the official General registry at the last Julia minor they support. This prevents package resolution and lower-bound testing from selecting an old, incompatible JET.

Test environments can remain instantiable on unsupported Julia versions, and JET-specific tests can be skipped at runtime using the JET_AVAILABLE constant:

using JET

if JET.JET_AVAILABLE
    include("jet_tests.jl")
end
JET.JET_AVAILABLEConstant
const JET_AVAILABLE::Bool

Whether full JET functionality is available in the current process.

This is true on supported Julia versions, when JET_DEV_MODE is enabled, or when running under PkgEval, i.e. with the JULIA_PKGEVAL environment variable set. Otherwise JET is loaded with empty stubs: loading it emits a warning, and calling any of its entry points throws an error.

This lets a test suite stay instantiable on unsupported Julia versions while skipping its JET-specific checks at runtime:

using JET

if JET.JET_AVAILABLE
    include("jet_tests.jl")
end
source

Setting the JET_DEV_MODE preference to true forces JET to try loading full functionality on an unsupported Julia version.

JET.JET_DEV_MODEConstant
const JET_DEV_MODE::Bool

Whether JET is loaded in development mode.

This is a Preferences.jl setting that is read when JET is loaded, so it needs to be configured before JET is precompiled, e.g. with a LocalPreferences.toml file containing:

[JET]
JET_DEV_MODE = true

Enabling it has the following effects:

  • Full JET functionality is loaded even on unsupported Julia versions, i.e. JET_AVAILABLE becomes true. Note that JET may still not work correctly on such versions.
  • Internal assertions that check JET's report caching and validate report interface implementations are compiled in. They are omitted by default since they add overhead.
  • The use_fixed_world preference defaults to false, so JET's pre-defined analyzers and interpreters run their analysis in the current world age instead of the world age fixed at load time. This makes redefinitions of JET's own code take effect, at the cost of losing robustness against invalidations caused by loading other packages.

This mode is intended for developing JET itself and for experimenting on unsupported Julia versions, and is not recommended otherwise.

source

Quickstart

See more commands, options, and explanations in the documentation.

Installation

JET is a standard Julia package, so you can install it via Julia's built-in package manager and use it just like any other package:

julia> using Pkg; Pkg.add("JET")
[ some output elided ]

julia> using JET
Note

The package manager installs a JET version allowed by registry metadata and the compatibility constraints of your environment. This does not necessarily mean full JET functionality is supported on your Julia version; check JET.JET_AVAILABLE after loading JET.

Existing dependencies may also prevent a working JET version from being installed. This is particularly likely when the version of JuliaInterpreter.jl is incompatible with JET, since JuliaInterpreter is also a dependency of the very commonly used package Revise.jl. In such cases, the most reliable way to install and use a working JET is to set up a temporary environment (e.g., Pkg.activate(; temp=true)) and use JET there.

Detect type instability with @report_opt

Type instabilities can be detected in function calls using the @report_opt macro, which works similarly to the @code_warntype macro. Note that, because JET relies on Julia's type inference, it cannot see through unresolved dynamic dispatch: callees reached only through such calls are not analyzed, so problems inside them go unreported.

julia> @report_opt foldl(+, Any[]; init=0)═════ 2 possible errors found ═════
kwcall(::@NamedTuple{init::Int64}, ::typeof(foldl), op::typeof(+), itr::Vector{Any}) @ Base ./reduce.jl:190
foldl(op::typeof(+), itr::Vector{Any}; kw::@Kwargs{init::Int64}) @ Base ./reduce.jl:190
kwcall(::@NamedTuple{init::Int64}, ::typeof(mapfoldl), f::typeof(identity), op::typeof(+), itr::Vector{Any}) @ Base ./reduce.jl:167
mapfoldl(f::typeof(identity), op::typeof(+), itr::Vector{Any}; init::Int64) @ Base ./reduce.jl:167
mapfoldl_impl(f::typeof(identity), op::typeof(+), nt::Int64, itr::Vector{Any}) @ Base ./reduce.jl:36
foldl_impl(op::Base.BottomRF{typeof(+)}, nt::Int64, itr::Vector{Any}) @ Base ./reduce.jl:40
_foldl_impl(op::Base.BottomRF{typeof(+)}, init::Int64, itr::Vector{Any}) @ Base ./reduce.jl:50
(::Base.BottomRF{typeof(+)})(acc::Int64, x::Any) @ Base ./reduce.jl:78
│ runtime dispatch detected: +(acc::Int64, x::Any)::Any
└────────────────────
_foldl_impl(op::Base.BottomRF{typeof(+)}, init::Int64, itr::Vector{Any}) @ Base ./reduce.jl:54
(::Base.BottomRF{typeof(+)})(acc::Any, x::Any) @ Base ./reduce.jl:78
│ runtime dispatch detected: +(acc::Any, x::Any)::Any
└────────────────────

Detect type errors with @report_call

While @report_opt detects performance problems, @report_call detects potential bugs: calls that may throw at runtime, such as MethodErrors. Since JET cannot see through unresolved dynamic dispatch, fixing the instabilities reported by @report_opt first lets @report_call cover more of your code. That said, @report_call is often less noisy than @report_opt, so it is also perfectly reasonable to start with @report_call alone.

julia> @report_call foldl(+, Char[])═════ 2 possible errors found ═════
foldl(op::typeof(+), itr::Vector{Char}) @ Base ./reduce.jl:190
foldl(op::typeof(+), itr::Vector{Char}; kw::@Kwargs{}) @ Base ./reduce.jl:190
mapfoldl(f::typeof(identity), op::typeof(+), itr::Vector{Char}) @ Base ./reduce.jl:167
mapfoldl(f::typeof(identity), op::typeof(+), itr::Vector{Char}; init::Base._InitialValue) @ Base ./reduce.jl:167
mapfoldl_impl(f::typeof(identity), op::typeof(+), nt::Base._InitialValue, itr::Vector{Char}) @ Base ./reduce.jl:36
foldl_impl(op::Base.BottomRF{typeof(+)}, nt::Base._InitialValue, itr::Vector{Char}) @ Base ./reduce.jl:40
_foldl_impl(op::Base.BottomRF{typeof(+)}, init::Base._InitialValue, itr::Vector{Char}) @ Base ./reduce.jl:54
(::Base.BottomRF{typeof(+)})(acc::Char, x::Char) @ Base ./reduce.jl:78
│ no matching method found `+(::Char, ::Char)`: (op::Base.BottomRF{typeof(+)}).rf::typeof(+)(acc::Char, x::Char)
└────────────────────
foldl_impl(op::Base.BottomRF{typeof(+)}, nt::Base._InitialValue, itr::Vector{Char}) @ Base ./reduce.jl:41
reduce_empty_iter(op::Base.BottomRF{typeof(+)}, itr::Vector{Char}) @ Base ./reduce.jl:373
reduce_empty_iter(op::Base.BottomRF{typeof(+)}, itr::Vector{Char}, ::Base.HasEltype) @ Base ./reduce.jl:374
reduce_empty(op::Base.BottomRF{typeof(+)}, ::Type{Char}) @ Base ./reduce.jl:350
reduce_empty(::typeof(+), ::Type{Char}) @ Base ./reduce.jl:336
│ no matching method found `zero(::Type{Char})`: zero(T::Type{Char})
└────────────────────

Analyze packages with report_package

This looks for all method definitions and analyzes function calls based on their signatures. Note that this is less accurate than @report_call, because the actual input types cannot be known for generic methods.

julia> using Pkg; Pkg.activate(; temp=true, io=devnull); Pkg.add("AbstractTrees"; io=devnull);
julia> Pkg.status()Status `/tmp/jl_uMe8jo/Project.toml` [1520ce14] AbstractTrees v0.4.5
julia> using AbstractTrees
julia> report_package(AbstractTrees, toplevel_logger=nothing)═════ 6 possible errors found ═════ isroot(root::Any, x::Any) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/base.jl:102 │ no matching method found `parent(::Any, ::Any)`: AbstractTrees.parent(root::Any, x::Any) └──────────────────── AbstractTrees.IndexNode(tree::Any) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/indexing.jl:117 │ no matching method found `rootindex(::Any)`: rootindex(tree::Any) └──────────────────── parent(idx::AbstractTrees.IndexNode) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/indexing.jl:127 │ no matching method found `parentindex(::Any, ::Any)`: pidx = parentindex((idx::AbstractTrees.IndexNode).tree::Any, (idx::AbstractTrees.IndexNode).index::Any) └──────────────────── nextsibling(idx::AbstractTrees.IndexNode) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/indexing.jl:132 │ no matching method found `nextsiblingindex(::Any, ::Any)`: sidx = nextsiblingindex((idx::AbstractTrees.IndexNode).tree::Any, (idx::AbstractTrees.IndexNode).index::Any) └──────────────────── prevsibling(idx::AbstractTrees.IndexNode) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/indexing.jl:137 │ no matching method found `prevsiblingindex(::Any, ::Any)`: sidx = prevsiblingindex((idx::AbstractTrees.IndexNode).tree::Any, (idx::AbstractTrees.IndexNode).index::Any) └──────────────────── prevsibling(csr::AbstractTrees.IndexedCursor) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/cursors.jl:234 │ no matching method found `getindex(::Nothing, ::Int64)` (1/2 union split): (AbstractTrees.parent(csr::AbstractTrees.IndexedCursor)::Union{Nothing, AbstractTrees.IndexedCursor})[idx::Int64] └────────────────────
julia> report_package(AbstractTrees; target_modules=(AbstractTrees,), toplevel_logger=nothing) # ignore errors that occur outside the AbstractTrees module context═════ 6 possible errors found ═════ isroot(root::Any, x::Any) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/base.jl:102 │ no matching method found `parent(::Any, ::Any)`: AbstractTrees.parent(root::Any, x::Any) └──────────────────── AbstractTrees.IndexNode(tree::Any) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/indexing.jl:117 │ no matching method found `rootindex(::Any)`: rootindex(tree::Any) └──────────────────── parent(idx::AbstractTrees.IndexNode) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/indexing.jl:127 │ no matching method found `parentindex(::Any, ::Any)`: pidx = parentindex((idx::AbstractTrees.IndexNode).tree::Any, (idx::AbstractTrees.IndexNode).index::Any) └──────────────────── nextsibling(idx::AbstractTrees.IndexNode) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/indexing.jl:132 │ no matching method found `nextsiblingindex(::Any, ::Any)`: sidx = nextsiblingindex((idx::AbstractTrees.IndexNode).tree::Any, (idx::AbstractTrees.IndexNode).index::Any) └──────────────────── prevsibling(idx::AbstractTrees.IndexNode) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/indexing.jl:137 │ no matching method found `prevsiblingindex(::Any, ::Any)`: sidx = prevsiblingindex((idx::AbstractTrees.IndexNode).tree::Any, (idx::AbstractTrees.IndexNode).index::Any) └──────────────────── prevsibling(csr::AbstractTrees.IndexedCursor) @ AbstractTrees /home/runner/.julia/packages/AbstractTrees/Ftf8W/src/cursors.jl:234 │ no matching method found `getindex(::Nothing, ::Int64)` (1/2 union split): (AbstractTrees.parent(csr::AbstractTrees.IndexedCursor)::Union{Nothing, AbstractTrees.IndexedCursor})[idx::Int64] └────────────────────

Limitations

JET explores the functions you call directly as well as their inferable callees. However, if the argument types for a call cannot be inferred, JET does not analyze the callee. Consequently, a report of No errors detected does not imply that your entire codebase is free of errors. To increase confidence in JET's results, use @report_opt to make sure your code is inferable.

Acknowledgements

This project started as my undergraduate thesis at Kyoto University, supervised by Prof. Takashi Sakuragawa. It was heavily inspired by ruby/typeprof, an experimental type understanding/checking tool for Ruby. The thesis is published at https://github.com/aviatesk/grad-thesis, but currently it's only available in Japanese.