JET.jl
JET employs Julia's type inference system to detect potential bugs and type instabilities.
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.
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")
endJET.JET_AVAILABLE — Constant
const JET_AVAILABLE::BoolWhether 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")
endSetting the JET_DEV_MODE preference to true forces JET to try loading full functionality on an unsupported Julia version.
JET.JET_DEV_MODE — Constant
const JET_DEV_MODE::BoolWhether 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 = trueEnabling it has the following effects:
- Full JET functionality is loaded even on unsupported Julia versions, i.e.
JET_AVAILABLEbecomestrue. 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_worldpreference defaults tofalse, 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.
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 JETThe 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.5julia> using AbstractTreesjulia> 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.