Error analysis

Julia's type system is quite expressive, and its type inference is strong enough to generate fairly optimized code from a highly generic program written in a concise syntax. Unlike other statically compiled languages, however, Julia by design does not report an error or warning when it detects possible errors during compilation, no matter how serious they are. In essence, Julia achieves highly generic and composable programming by delaying all errors and warnings until runtime.

This is a core design choice of the language. On the one hand, Julia's dynamism allows it to work in places where data types cannot be fully decided ahead of runtime (e.g., when the program is duck-typed with generic pieces of code, or when the program consumes some data that is only known at runtime). On the other hand, with Julia, it is not straightforward to have the modern development experiences that a static language can typically offer, such as static type checking and rich IDE features.

JET is a trial to get the best of both worlds: can we have sufficiently useful static checking without losing all the beauty of Julia's dynamism and composability? JET's approach is very different from "gradual typing", which is a common technique for bringing static analysis into a dynamic language, as used by, e.g., mypy for Python and TypeScript for JavaScript. Rather, JET's static analysis is powered by Julia's built-in type inference system, which is based on a technique called "abstract interpretation". JET can therefore analyze a normal Julia program and detect possible errors without analysis-only type annotations, preserving the program's original polymorphism and composability. Its effectiveness depends on the precision of Julia's type inference, which also powers compiler optimization.

Quick start

julia> using JET

Let's start with the simplest example: how can JET find anything wrong with sum("julia")? @report_call and report_call analyze a given function call and report possible problems. They can be used similarly to @code_typed and code_typed. These interactive entry points are the easiest way to use JET:

julia> @report_call sum("julia")═════ 2 possible errors found ═════
sum(a::String) @ Base ./reduce.jl:554
sum(a::String; kw::@Kwargs{}) @ Base ./reduce.jl:554
sum(f::typeof(identity), a::String) @ Base ./reduce.jl:525
sum(f::typeof(identity), a::String; kw::@Kwargs{}) @ Base ./reduce.jl:525
mapreduce(f::typeof(identity), op::typeof(Base.add_sum), itr::String) @ Base ./reduce.jl:300
mapreduce(f::typeof(identity), op::typeof(Base.add_sum), itr::String; kw::@Kwargs{}) @ Base ./reduce.jl:300
mapfoldl(f::typeof(identity), op::typeof(Base.add_sum), itr::String) @ Base ./reduce.jl:167
mapfoldl(f::typeof(identity), op::typeof(Base.add_sum), itr::String; init::Base._InitialValue) @ Base ./reduce.jl:167
mapfoldl_impl(f::typeof(identity), op::typeof(Base.add_sum), nt::Base._InitialValue, itr::String) @ Base ./reduce.jl:36
foldl_impl(op::Base.BottomRF{typeof(Base.add_sum)}, nt::Base._InitialValue, itr::String) @ Base ./reduce.jl:40
_foldl_impl(op::Base.BottomRF{typeof(Base.add_sum)}, init::Base._InitialValue, itr::String) @ Base ./reduce.jl:54
(::Base.BottomRF{typeof(Base.add_sum)})(acc::Char, x::Char) @ Base ./reduce.jl:78
add_sum(x::Char, y::Char) @ Base ./reduce.jl:16
│ no matching method found `+(::Char, ::Char)`: (x::Char + y::Char)
└────────────────────
foldl_impl(op::Base.BottomRF{typeof(Base.add_sum)}, nt::Base._InitialValue, itr::String) @ Base ./reduce.jl:41
reduce_empty_iter(op::Base.BottomRF{typeof(Base.add_sum)}, itr::String) @ Base ./reduce.jl:373
reduce_empty_iter(op::Base.BottomRF{typeof(Base.add_sum)}, itr::String, ::Base.HasEltype) @ Base ./reduce.jl:374
reduce_empty(op::Base.BottomRF{typeof(Base.add_sum)}, ::Type{Char}) @ Base ./reduce.jl:350
reduce_empty(::typeof(Base.add_sum), ::Type{Char}) @ Base ./reduce.jl:343
reduce_empty(::typeof(+), ::Type{Char}) @ Base ./reduce.jl:336
│ no matching method found `zero(::Type{Char})`: zero(T::Type{Char})
└────────────────────

So JET found two possible problems. Now let's see how they can occur in actual execution:

julia> sum("julia") # will lead to `MethodError: +(::Char, ::Char)`ERROR: MethodError: no method matching +(::Char, ::Char)
The function `+` exists, but no method is defined for this combination of argument types.

Closest candidates are:
  +(::Any, ::Any, ::Any, ::Any...)
   @ Base operators.jl:642
  +(::T, ::Integer) where T<:AbstractChar
   @ Base char.jl:247
  +(::Integer, ::AbstractChar)
   @ Base char.jl:257
  ...
julia> sum("") # will lead to `MethodError: zero(Type{Char})`ERROR: MethodError: no method matching zero(::Type{Char}) The function `zero` exists, but no method is defined for this combination of argument types. Closest candidates are: zero(::Type{Union{}}, Any...) @ Base number.jl:315 zero(::Type{Dates.Time}) @ Dates /opt/hostedtoolcache/julia/1.12-nightly/x64/share/julia/stdlib/v1.12/Dates/src/types.jl:460 zero(::Type{Pkg.Resolve.FieldValue}) @ Pkg /opt/hostedtoolcache/julia/1.12-nightly/x64/share/julia/stdlib/v1.12/Pkg/src/Resolve/fieldvalues.jl:40 ...

Note that @report_call sum("julia") could detect both of those different errors that can happen at runtime. This is because @report_call performs static analysis: it analyzes the function call in a way that does not rely on one instance of runtime execution, but rather reasons about all possible executions. This is one of the biggest advantages of static analysis because other ways to check software quality, such as testing, usually rely on some runtime execution and can cover only a subset of all possible executions.

As mentioned above, JET is designed to work with a normal Julia program. Let's define new arbitrary functions and run JET on them:

julia> function foo(s0)
           a = []
           for s in split(s0)
               push!(a, bar(s))
           end
           return sum(a)
       endfoo (generic function with 1 method)
julia> bar(s::String) = parse(Int, s)bar (generic function with 1 method)
julia> @report_call foo("1 2 3")═════ 2 possible errors found ═════ foo(s0::String) @ Main ./REPL[1]:4 │ no matching method found `bar(::SubString{String})`: Main.bar(s) └──────────────────── foo(s0::String) @ Main ./REPL[1]:6 sum(a::Vector{Any}) @ Base ./reducedim.jl:979 sum(a::Vector{Any}; dims::Colon, kw::@Kwargs{}) @ Base ./reducedim.jl:979 _sum(a::Vector{Any}, ::Colon) @ Base ./reducedim.jl:983 _sum(a::Vector{Any}, ::Colon; kw::@Kwargs{}) @ Base ./reducedim.jl:983 _sum(f::typeof(identity), a::Vector{Any}, ::Colon) @ Base ./reducedim.jl:984 _sum(f::typeof(identity), a::Vector{Any}, ::Colon; kw::@Kwargs{}) @ Base ./reducedim.jl:984 mapreduce(f::typeof(identity), op::typeof(Base.add_sum), A::Vector{Any}) @ Base ./reducedim.jl:326 mapreduce(f::typeof(identity), op::typeof(Base.add_sum), A::Vector{Any}; dims::Colon, init::Base._InitialValue) @ Base ./reducedim.jl:326 _mapreduce_dim(f::typeof(identity), op::typeof(Base.add_sum), ::Base._InitialValue, A::Vector{Any}, ::Colon) @ Base ./reducedim.jl:334 _mapreduce(f::typeof(identity), op::typeof(Base.add_sum), ::IndexLinear, A::Vector{Any}) @ Base ./reduce.jl:422 mapreduce_empty_iter(f::typeof(identity), op::typeof(Base.add_sum), itr::Vector{Any}, ItrEltype::Base.HasEltype) @ Base ./reduce.jl:370 reduce_empty_iter(op::Base.MappingRF{typeof(identity), typeof(Base.add_sum)}, itr::Vector{Any}, ::Base.HasEltype) @ Base ./reduce.jl:374 reduce_empty(op::Base.MappingRF{typeof(identity), typeof(Base.add_sum)}, ::Type{Any}) @ Base ./reduce.jl:351 mapreduce_empty(::typeof(identity), op::typeof(Base.add_sum), T::Type{Any}) @ Base ./reduce.jl:362 reduce_empty(::typeof(Base.add_sum), ::Type{Any}) @ Base ./reduce.jl:343 reduce_empty(::typeof(+), ::Type{Any}) @ Base ./reduce.jl:336 zero(::Type{Any}) @ Base ./missing.jl:106 │ MethodError: no method matching zero(::Type{Any}): throw(MethodError((Base).zero::typeof(zero), tuple(Any)::Tuple{DataType})::MethodError) └────────────────────

Now let's fix this problematic code. First, we can fix the definition of bar so that it accepts generic AbstractString input. JET's analysis result can be dynamically updated when we refine a function definition, so we just need to add a new bar(::AbstractString) definition.

As for the second error, let's assume that, for some reason, we are not interested in fixing it and want to ignore errors that may happen within Base. We can use the ignored_modules configuration to exclude Base from the analysis scope and ignore the possible error that may happen within sum(a)[1].

julia> # hot-fix the definition of `bar`
       bar(s::AbstractString) = parse(Int, s)bar (generic function with 2 methods)
julia> # now no errors should be reported! @report_call ignored_modules=(Base,) foo("1 2 3")No errors detected

The complementary target_modules configuration keeps only the reports that match. A module given to these configurations matches that module and its submodules, where the matching stops at namespace roots: Base and package root modules do not count as submodules of Main. Since code defined interactively in the REPL lives in Main, we can get the same effect by retaining Main only:

julia> @report_call target_modules=(Main,) foo("1 2 3")No errors detected

So far, we have used the default error analysis mode, which collects problems according to one specific, somewhat opinionated definition of "errors". JET offers other analysis modes, including "sound" error detection and the simpler "typo" detection mode (see JETAnalyzer for an overview). They can be switched using the mode configuration:

julia> function myifelse(cond, a, b)
           if cond
               return a
           else
               return b
           end
       endmyifelse (generic function with 1 method)
julia> # the default analysis pass doesn't report a # "non-boolean `T` found in boolean context" error if the condition could be # `Bool` (note that `Bool <: Integer`) report_call(myifelse, (Integer, Int, Int))No errors detected
julia> # the sound analyzer requires the type of a conditional value to be strictly # `Bool` report_call(myifelse, (Integer, Int, Int); mode=:sound)═════ 1 possible error found ═════ myifelse(cond::Integer, a::Int64, b::Int64) @ Main ./REPL[1]:2 │ non-boolean `Integer` may be used in boolean context: goto %4 if not cond::Integer └────────────────────
julia> function strange_sum(a) if rand(Bool) undefsum(a) else sum(a) end endstrange_sum (generic function with 1 method)
julia> # the default analysis pass will report both problems: # - `undefsum` is not defined # - `sum(a::Vector{Any})` can throw when `a` is empty @report_call strange_sum([])═════ 2 possible errors found ═════ strange_sum(a::Vector{Any}) @ Main ./REPL[4]:3 │ `Main.undefsum` is not defined: Main.undefsum └──────────────────── strange_sum(a::Vector{Any}) @ Main ./REPL[4]:5 sum(a::Vector{Any}) @ Base ./reducedim.jl:979 sum(a::Vector{Any}; dims::Colon, kw::@Kwargs{}) @ Base ./reducedim.jl:979 _sum(a::Vector{Any}, ::Colon) @ Base ./reducedim.jl:983 _sum(a::Vector{Any}, ::Colon; kw::@Kwargs{}) @ Base ./reducedim.jl:983 _sum(f::typeof(identity), a::Vector{Any}, ::Colon) @ Base ./reducedim.jl:984 _sum(f::typeof(identity), a::Vector{Any}, ::Colon; kw::@Kwargs{}) @ Base ./reducedim.jl:984 mapreduce(f::typeof(identity), op::typeof(Base.add_sum), A::Vector{Any}) @ Base ./reducedim.jl:326 mapreduce(f::typeof(identity), op::typeof(Base.add_sum), A::Vector{Any}; dims::Colon, init::Base._InitialValue) @ Base ./reducedim.jl:326 _mapreduce_dim(f::typeof(identity), op::typeof(Base.add_sum), ::Base._InitialValue, A::Vector{Any}, ::Colon) @ Base ./reducedim.jl:334 _mapreduce(f::typeof(identity), op::typeof(Base.add_sum), ::IndexLinear, A::Vector{Any}) @ Base ./reduce.jl:422 mapreduce_empty_iter(f::typeof(identity), op::typeof(Base.add_sum), itr::Vector{Any}, ItrEltype::Base.HasEltype) @ Base ./reduce.jl:370 reduce_empty_iter(op::Base.MappingRF{typeof(identity), typeof(Base.add_sum)}, itr::Vector{Any}, ::Base.HasEltype) @ Base ./reduce.jl:374 reduce_empty(op::Base.MappingRF{typeof(identity), typeof(Base.add_sum)}, ::Type{Any}) @ Base ./reduce.jl:351 mapreduce_empty(::typeof(identity), op::typeof(Base.add_sum), T::Type{Any}) @ Base ./reduce.jl:362 reduce_empty(::typeof(Base.add_sum), ::Type{Any}) @ Base ./reduce.jl:343 reduce_empty(::typeof(+), ::Type{Any}) @ Base ./reduce.jl:336 zero(::Type{Any}) @ Base ./missing.jl:106 │ MethodError: no method matching zero(::Type{Any}): throw(MethodError((Base).zero::typeof(zero), tuple(Any)::Tuple{DataType})::MethodError) └────────────────────
julia> # the typo detection pass will only report the "typo" @report_call mode=:typo strange_sum([])═════ 1 possible error found ═════ strange_sum(a::Vector{Any}) @ Main ./REPL[4]:3 │ `Main.undefsum` is not defined: Main.undefsum └────────────────────

We can use @test_call and test_call to assert that your program is free from problems that @report_call can detect. They work nicely with the unit-testing infrastructure of Julia's Test standard library:

julia> @test_call ignored_modules=(Base,) foo("1 2 3")Test Passed
julia> using Test
julia> # we can get a nice summary using `@testset`! @testset "JET testset" begin @test_call ignored_modules=(Base,) foo("1 2 3") # should pass test_call(myifelse, (Integer, Int, Int); mode=:sound) @test_call broken=true foo("1 2 3") # `broken` and `skip` options are supported @test foo("1 2 3") == 6 # other `Test` macros can be used in the same place endJET testset: JET-test failed at /home/runner/work/JET.jl/JET.jl/src/JETBase.jl:1312 Expression: (JET.report_call)(Main.myifelse, (Integer, Int64, Int64); mode = sound) ═════ 1 possible error found ═════ myifelse(cond::Integer, a::Int64, b::Int64) @ Main ./REPL[1]:2 │ non-boolean `Integer` may be used in boolean context: goto %4 if not cond::Integer └──────────────────── Test Summary: | Pass Fail Broken Total Time JET testset | 2 1 1 4 3.5s RNG of the outermost testset: Random.Xoshiro(0x07c34f585007dbcd, 0x6f44191c5711ee67, 0xc10f10225de5ebff, 0x014edc0513245085, 0x2402ef7cb0b976a0) ERROR: Some tests did not pass: 2 passed, 1 failed, 0 errored, 1 broken.

JET uses itself in its test pipeline: JET's static analysis has been proven to be very useful and has helped its development a lot. If interested, take a peek at JET's "self check" testset.

Lastly, let's see an example that demonstrates that JET can analyze a "top-level" program. Top-level analysis should be considered somewhat experimental, and currently you may need additional configurations to run it correctly. Please read the descriptions of top-level entry points and choose an appropriate entry point for your use case. Here we run report_file on demo.jl. It automatically extracts and loads "definitions" of functions, structs, and such, and then analyzes their "usages" statically:

julia> report_file(normpath(Base.pkgdir(JET), "demo.jl"))[toplevel-info] virtualized the context of Main (took 0.015 sec)
[toplevel-info] entered into /home/runner/work/JET.jl/JET.jl/demo.jl
[toplevel-info]  exited from /home/runner/work/JET.jl/JET.jl/demo.jl (took 0.326 sec)
═════ 6 possible errors found ═════
Toplevel MethodInstance thunk @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:9
│ `m` is not defined: m
└────────────────────
Toplevel MethodInstance thunk @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:10
fib(n::String) @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:6
<=(x::String, y::Int64) @ Base ./operators.jl:448
<(x::String, y::Int64) @ Base ./operators.jl:399
│ no matching method found `isless(::String, ::Int64)`: isless(x::String, y::Int64)
└────────────────────
Toplevel MethodInstance thunk @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:15
getproperty(x::Bool, f::Symbol) @ Base ./Base_compiler.jl:54
│ invalid builtin function call: getfield(x::Bool, f::Symbol)
└────────────────────
Toplevel MethodInstance thunk @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:28
foo(a::Float64) @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:20
bar(v::Ty{Float64}) @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:25
getproperty(x::Ty{Float64}, f::Symbol) @ Base ./Base_compiler.jl:54
│ FieldError: type Ty has no field `fdl`, available fields: `fld`: getfield(x::Ty{Float64}, f::Symbol)
└────────────────────
Toplevel MethodInstance thunk @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:29
foo(a::String) @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:20
bar(v::Ty{String}) @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:26
│ no matching method found `convert(::Type{Number}, ::String)`: convert(Number, (v::Ty{String}).fld::String)
└────────────────────
Toplevel MethodInstance thunk @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:40
badmerge(a::@NamedTuple{x::Int64, y::Int64}, b::@NamedTuple{y::Int64, z::Int64}) @ Main /home/runner/work/JET.jl/JET.jl/demo.jl:33
│ `x` is not defined: x
└────────────────────

Error kinds and how to fix them

no matching method found

Description

This error occurs when running the code might throw a MethodError at runtime. Similar to regular MethodErrors, this happens if a function is called without an applicable method for the given argument types.

This is the most common error detected in most Julia code.

Example

julia> f(x::Integer) = x + one(x);
julia> g(x) = f(x);
julia> @report_call g(1.0)═════ 1 possible error found ═════ g(x::Float64) @ Main ./REPL[2]:1 │ no matching method found `f(::Float64)`: Main.f(x::Float64) └────────────────────

How to fix

This error indicates some kind of type error in your code. Fix it as you would a regular MethodError thrown at runtime.

no matching method found (x/y union split)

Description

This error occurs when a variable x is inferred to be a union type, and x being one or more of the union's members would lead to a MethodError. For example, suppose the compiler infers x to be of type Union{A, B} and the code then calls f(x). This error occurs if the call would lead to a MethodError when x is an A.

More technically, this happens when one or more branches created by the compiler through union splitting contain a no matching method found error.

Example

Minimal example:

julia> struct Foo
           x::Union{Int, String}
       end
julia> # Errors if x.x isa String. # The compiler doesn't know if it's a String or Int f(x) = x.x + 1;
julia> @report_call f(Foo(1))═════ 1 possible error found ═════ f(x::Main.Foo) @ Main ./REPL[2]:1 │ no matching method found `+(::String, ::Int64)` (1/2 union split): ((x::Main.Foo).x::Union{Int64, String} Main.:+ 1) └────────────────────

More common example:

julia> function pos_after_tab(v::AbstractArray{UInt8})
           # findfirst can return `nothing` on no match
           p = findfirst(isequal(UInt8('\t')), v)
           p + 1
       endpos_after_tab (generic function with 1 method)
julia> @report_call pos_after_tab(codeunits("a\tb"))═════ 1 possible error found ═════ pos_after_tab(v::Base.CodeUnits{UInt8, String}) @ Main ./REPL[1]:4 │ no matching method found `+(::Nothing, ::Int64)` (1/2 union split): (p Main.:+ 1) └────────────────────

How to fix

This error is unique in that idiomatic Julia code may still lead to it. For example, in the pos_after_tab function above, if the input vector does not have a '\t' byte, p will be nothing, and a MethodError will be thrown when nothing + 1 is attempted. However, in many situations, the possibility of such a MethodError is not a mistake but rather an idiomatic way of raising an error.

There are different possibilities for addressing this kind of error. Let's take the pos_after_tab example.

If you could legitimately expect p to be nothing for valid input (i.e., the input could lack a '\t' byte), then your function should be written to take this edge case into account:

julia> function pos_after_tab(v::AbstractArray{UInt8})
           p = findfirst(isequal(UInt8('\t')), v)
           if p === nothing # handle the nothing case
               return nothing
           else
               return p + 1
           end
       end;
julia> @report_call pos_after_tab(codeunits("a\tb"))No errors detected

By adding the if p === nothing check, the compiler will know that the type of p must be Nothing inside the if block and Int in the else block. This way, the compiler knows a MethodError is not possible, and the error will disappear.

If you expect a '\t' byte to always be present, such that findfirst should always return an Int for valid input, you can add a type assertion in the function to assert that the return value of findfirst must be, say, an Integer. Then, the compiler will know that if the type assertion passes, the value returned by findfirst cannot be nothing and hence, in this case, must be an Int:

julia> function pos_after_tab(v::AbstractArray{UInt8})
           p = findfirst(isequal(UInt8('\t')), v)::Integer
           p + 1
       end;
julia> @report_call pos_after_tab(codeunits("a\tb"))No errors detected

The code will still error at runtime due to the type assertion if findfirst returns nothing, but JET will no longer detect it as an error because the programmer, by adding the type assertion, explicitly acknowledges that the compiler's inference may not be precise enough and helps the compiler.

Note that adding a type assertion also improves code quality:

  • The programmer's intent never to observe nothing is communicated clearly.
  • After the type assertion passes, p is inferred to be Int instead of a union, and this more precise type inference generates more efficient code.
  • More precise inference reduces the risk of invalidations from the code, improving latency.

A special case occurs when loading Union-typed fields from structs. Julia does not realize that loading the same field multiple times from a mutable struct necessarily returns the same object. Hence, consider the following example:

julia> mutable struct Foo
           x::Union{Int, Nothing}
       end
julia> function f(x) if x.x === nothing nothing else x.x + 1 end end;
julia> @report_call f(Foo(1))═════ 1 possible error found ═════ f(x::Main.Foo) @ Main ./REPL[2]:5 │ no matching method found `+(::Nothing, ::Int64)` (1/2 union split): ((x::Main.Foo).x::Union{Nothing, Int64} Main.:+ 1) └────────────────────

We might reasonably expect the compiler to know that in the else branch, x.x must be an Int, since it just checked that it is not nothing. However, the compiler does not know that the value obtained from loading the x field in the x.x expression on the line with the if statement is the same value as the value obtained when loading the x field in the x.x + 1 statement. You can solve this issue by assigning x.x to a variable:

julia> function f(x)
           y = x.x
           if y === nothing
               nothing
           else
               y + 1
           end
       end;
julia> @report_call f(Foo(1))No errors detected

X is not defined

Description

This happens when a name X is used in a function, but no object named X can be found.

Example

julia> f(x) = foo(x) + 1;
julia> @report_call f(1)═════ 1 possible error found ═════ f(x::Int64) @ Main ./REPL[1]:1 │ `Main.foo` is not defined: Main.foo └────────────────────

How to fix

This error can have a couple of causes:

  • X is misspelled. If so, correct the typo.
  • X exists but cannot be reached from the scope of the function. If so, pass it as an argument to the offending function.

type T has no field F

Description

This error occurs when Core.getfield is called, directly or indirectly, with a nonexistent hard-coded field name. For example, an object might have a field called vec, but you type vector.

Example

julia> struct Foo
           my_field
       end
julia> f(x) = x.my_feild; # NB: Typo!
julia> @report_call f(Foo(1))═════ 1 possible error found ═════ f(x::Main.Foo) @ Main ./REPL[2]:1 getproperty(x::Main.Foo, f::Symbol) @ Base ./Base_compiler.jl:54 │ FieldError: type Main.Foo has no field `my_feild`, available fields: `my_field`: getfield(x::Main.Foo, f::Symbol) └────────────────────

How to fix

This error often occurs when the field name is mistyped. Correct the typo.

BoundsError: Attempt to access T at index [i]

Description

This error occurs when it is known at compile time that the call will throw a BoundsError. Note that most BoundsErrors cannot be predicted at compile time. For the compiler to know that a function attempts to access a container out of bounds, both the container length and the index value must be known at compile time. Hence, the error is detected for a Tuple input in the example below, but not for a Vector input.

Example

julia> get_fourth(x) = x[4]get_fourth (generic function with 1 method)
julia> @report_call get_fourth((1,2,3))═════ 1 possible error found ═════ get_fourth(x::Tuple{Int64, Int64, Int64}) @ Main ./REPL[1]:1 getindex(t::Tuple{Int64, Int64, Int64}, i::Int64) @ Base ./tuple.jl:33 │ BoundsError: attempt to access Tuple{Int64, Int64, Int64} at index [4]: getfield(t::Tuple{Int64, Int64, Int64}, i::Int64, $(Expr(:boundscheck))) └────────────────────
julia> @report_call get_fourth([1,2,3]) # NB: False negative!No errors detected

How to fix

If this error appears, the offending code uses a bad index. Since the error most often occurs when the index is hard-coded, simply fix the index value.

may throw [...]

Description

This error indicates that JET detected the possibility of an exception. By default, JET will not report this error unless a function is inferred to always throw and the exception is not caught in a try statement. In "sound" mode, this error is reported if the function may throw.

Example

In this example, the function is known at compile time to throw an uncaught exception, so it is reported by default:

julia> f(x) = x isa Integer ? throw("Integer") : nothing;
julia> @report_call f(1)═════ 1 possible error found ═════ f(x::Int64) @ Main ./REPL[1]:1 │ may throw: Main.throw("Integer") └────────────────────

In this example, it is not known at compile time whether the function throws, so JET reports no errors by default. In sound mode, the error is reported.

julia> f(x) = x == 9873984732 ? nothing : throw("Bad value")f (generic function with 1 method)
julia> @report_call f(1)No errors detected
julia> @report_call mode=:sound f(1)═════ 1 possible error found ═════ f(x::Int64) @ Main ./REPL[1]:1 │ may throw: Main.throw("Bad value") └────────────────────

In this example, the exception is handled, so JET reports no errors by default. In sound mode, the error is reported:

julia> g() = throw();
julia> f() = try g() catch nothing end;
julia> f()
julia> @report_call f()No errors detected
julia> @report_call mode=:sound f()═════ 1 possible error found ═════ f() @ Main ./REPL[2]:2 g() @ Main ./REPL[1]:1 │ may throw: Main.throw() └────────────────────

Entry points

Interactive entry points

JET offers interactive analysis entry points that can be used similarly to code_typed and its family:

JET.report_callFunction
report_call(f, [types]; jetconfigs...) -> JETCallResult
report_call(tt::Type{<:Tuple}; jetconfigs...) -> JETCallResult
report_call(mi::Core.MethodInstance; jetconfigs...) -> JETCallResult

Analyzes a function call with the given type signature and returns a JETCallResult containing the detected type-level problems.

The general configurations and error-analysis-specific configurations can be supplied as keyword arguments.

See the documentation of the error analysis for details.

source

Top-level entry points

JET can also analyze your "top-level" program: it can take your Julia script or package and report possible errors.

JET analyzes your top-level program "half-statically": it selectively interprets and loads "definitions", such as function or struct definitions, and tries to simulate Julia's top-level code execution process. It tries to avoid executing any other parts of the code, such as function calls, and analyzes them based on abstract interpretation instead. This is where JET statically analyzes your code. If you are interested in how JET selects "top-level definitions", see JET.virtual_process.

Warning

Because JET interprets "definitions" in your code, that part of top-level analysis certainly runs your code. Note that JET can therefore cause side effects from your code. For example, JET will try to expand all macros used in your code, so side effects involved with macro expansions will also happen during JET's analysis process.

JET.report_fileFunction
report_file(file::AbstractString; jetconfigs...) -> JETToplevelResult

Analyzes file and returns a JETToplevelResult containing the detected type-level problems.

The general configurations and error-analysis-specific configurations can be supplied as keyword arguments.

Tip

When no files that call your package's functions are available, the analyze_from_definitions option can be useful because it lets JET analyze methods from their declared signatures. For example, JET can analyze itself this way:

# From the root directory of JET.jl
julia> report_file("src/JET.jl";
                   analyze_from_definitions = true)

See also report_package.

Note

This function enables toplevel_logger at the default logging level. You can override or disable it explicitly:

report_file(args...;
            toplevel_logger = nothing, # suppress the toplevel logger
            jetconfigs...) # other configurations

See JET's top-level analysis configurations for more details.

source
JET.report_packageFunction
report_package(package::Module; jetconfigs...) -> JETToplevelResult

Analyzes package and returns a JETToplevelResult containing the detected type-level problems.

This function uses Revise.jl to collect method signatures defined in the package and analyzes each method from its signature. This allows JET to analyze a package without requiring top-level call sites or usage examples.

The analysis is incremental. When the same package is analyzed multiple times, changes that Revise can handle are reflected in the results, while unaffected results from the initial analysis are reused. Subsequent analyses therefore often finish quickly, depending on the extent of invalidation.

The general configurations and JETAnalyzer-specific configurations can be supplied as keyword arguments. Supplied values override the defaults below:

  • ignore_missing_comparison = true: JET widens any inferred call result that is exactly Union{Bool,Missing} to Any. This reduces noise because report_package often begins analysis with imprecise argument types. Disable this configuration when the package intentionally handles missing, because it can hide errors that may occur at runtime.
  • ignore_throws = true: JET does not report errors from throw calls or exceptions that may propagate to callers. Package-level definitions often include intentional error-throwing interface functions, such as @noinline interface_func(::T) = error("Interface not implemented"), that do not indicate actual problems. Disable this configuration to analyze exception handling in the package.
About `target_modules` configuration

One of the most common issues of this analysis is that the results of report_package(pkg) can be overwhelmed by errors within pkg's dependency packages. In such cases, use the target_modules configuration to narrow down the error scope to pkg's module context:

julia> report_package(JET)
[toplevel-info] Analyzing top-level definition (progress: 815/815)
[toplevel-info] Analyzed all top-level definitions (all: 815 | analyzed: 815 | cached: 0 | took: 49.129 sec)
═════ 104 possible errors found ═════
... # Many type instabilities in Base Compiler are reported

julia> report_package(JET; target_modules=(JET,JET.JETInterface,JET.VSCode)) # Limit error reports to those occurring within JET module contexts
[toplevel-info] Skipped analysis for cached definition (815/815)
[toplevel-info] Analyzed all top-level definitions (all: 815 | analyzed: 0 | cached: 815 | took: 0.119 sec)
═════ 2 possible errors found ═════
...
source
JET.report_textFunction
report_text(text::AbstractString; jetconfigs...) -> JETToplevelResult
report_text(text::AbstractString, filename::AbstractString;
            jetconfigs...) -> JETToplevelResult

Analyzes the top-level code in text and returns a JETToplevelResult containing the detected type-level problems.

source

Test integration

JET also exports entry points that are fully integrated with the unit-testing infrastructure of Julia's Test standard library. They can be used in your test suite to assert that your program is free from errors that JET can detect:

JET.@test_callMacro
@test_call [jetconfigs...] [broken=false] [skip=false] f(args...)

Runs @report_call jetconfigs... f(args...) and records its result in the current test set. It records a Test.Pass when the call is free from detectable problems, a JET.JETTestFailure (a Test.Result that is tallied as a failure) when problems are detected, and a Test.Error when analysis throws an unexpected error. A JETTestFailure displays an abstract call stack for each reported problem.

julia> @test_call sincos(10)
Test Passed
  Expression: #= none:1 =# JET.@test_call sincos(10)

As with @report_call, the general configurations and error-analysis-specific configurations can be supplied as optional leading configuration arguments:

julia> cond = false

julia> function f(n)
           # `cond` is untyped, and will be reported by the sound analysis pass,
           # while JET's default analysis pass will ignore it
           if cond
               return n
           else
               return -n
           end
       end;

julia> @test_call f(10)
Test Passed
  Expression: #= none:1 =# JET.@test_call f(10)

julia> @test_call mode=:sound f(10)
JET-test failed at none:1
  Expression: #= none:1 =# JET.@test_call mode = :sound f(10)
  ═════ 1 possible error found ═════
  ┌ f(n::Int64) @ Main ./none:2
  │ non-boolean `Any` may be used in boolean context: goto %5 if not cond
  └────────────────────

ERROR: There was an error during testing

@test_call integrates with the unit-testing infrastructure of the Test standard library. Its result is included in the enclosing @testset summary, and it supports skip and broken annotations like the @test macro:

julia> using JET, Test

# Julia can't propagate the type constraint `ref[]::Number` to `sin(ref[])`,
# so JET reports a possible `MethodError`.
julia> f(ref) = isa(ref[], Number) ? sin(ref[]) : nothing;

# Extracting `ref[]` into a local variable `x` makes the call type-stable.
julia> g(ref) = (x = ref[]; isa(x, Number) ? sin(x) : nothing);

julia> @testset "check errors" begin
           ref = Ref{Union{Nothing,Int}}(0)
           @test_call f(ref)             # fail
           @test_call g(ref)             # pass
           @test_call broken=true f(ref) # broken; does not fail the test set
       end
check errors: JET-test failed at REPL[21]:3
  Expression: #= REPL[21]:3 =# JET.@test_call f(ref)
  ═════ 1 possible error found ═════
  ┌ f(ref::Base.RefValue{Union{Nothing, Int64}}) @ Main ./REPL[19]:1
  │ no matching method found `sin(::Nothing)` (1/2 union split): sin((ref::Base.RefValue{Union{Nothing, Int64}})[]::Union{Nothing, Int64})
  └────────────────────

Test Summary: | Pass  Fail  Broken  Total  Time
check errors  |    1     1       1      3  0.2s
ERROR: Some tests did not pass: 1 passed, 1 failed, 0 errored, 1 broken.
source
JET.test_callFunction
test_call(f, [types]; broken::Bool = false, skip::Bool = false, jetconfigs...)
test_call(tt::Type{<:Tuple}; broken::Bool = false, skip::Bool = false, jetconfigs...)

Runs report_call on a function call with the given type signature and tests that it is free from problems that report_call can detect. It behaves like @test_call, but accepts a type signature rather than a call expression.

source
JET.test_packageFunction
test_package(package::Module;
             broken::Bool = false, skip::Bool = false,
             toplevel_logger = nothing, jetconfigs...)

Runs report_package and tests that there are no problems detected.

As with report_package, the general configurations and error-analysis-specific configurations can be supplied as keyword arguments.

Like @test_call, test_package integrates with the Test standard library. See @test_call for details.

julia> using Example

julia> @testset "test_package" begin
           test_package(Example; toplevel_logger=nothing)
       end;
Test Summary: | Pass  Total  Time
test_package  |    1      1  0.0s
source
JET.test_textFunction
test_text(text::AbstractString;
          broken::Bool = false, skip::Bool = false, jetconfigs...)
test_text(text::AbstractString, filename::AbstractString;
          broken::Bool = false, skip::Bool = false, jetconfigs...)

Runs report_text and tests that there are no problems detected.

As with report_text, the general configurations and error-analysis-specific configurations can be supplied as keyword arguments.

Like @test_call, test_text integrates with the Test standard library. See @test_call for details.

source

JETAnalyzer

JET.JETAnalyzerType
abstract type JETAnalyzer <: ToplevelAbstractAnalyzer end

JET's default error analyzer, which powers the error analysis entry points such as report_call and report_file. The analyzer runs Julia's type inference on the given code and, while walking the inferred call graph, collects places that may raise runtime errors, such as MethodErrors and undefined name references.

There is no single correct definition of what should count as an "error" in this kind of static analysis: a stricter definition catches more potential problems, but also produces more false positives. JETAnalyzer therefore offers multiple analysis modes, each with its own error definition. JETAnalyzer itself is an abstract type; each mode is implemented as a concrete subtype, and the JETAnalyzer(; jetconfigs...) constructor selects one according to the mode configuration:

  • mode = :basic (default) constructs BasicJETAnalyzer, which reports problems that are likely to be actual errors, and is tuned to be useful for general Julia development. It is not strict enough to guarantee that the analyzed code is free from runtime errors.
  • mode = :sound constructs SoundJETAnalyzer, which reports any possibility of a runtime error covered by JET's error model, at the cost of more false positives. If it reports no errors, the analyzed code should not raise a runtime error covered by the model.
  • mode = :typo constructs TypoJETAnalyzer, which reports only a focused subset of likely typos, such as undefined name references and invalid field accesses. It is useful for large codebases where even the basic mode is too noisy.

Each subtype implements its own definition of an "error" by overloading the report hooks (report_method_error!, report_undef_global_var!, and so on), while sharing the traversal implemented for JETAnalyzer. JETAnalyzer is built on JET's AbstractAnalyzer framework, which third-party analyzers can also use to implement different analyses on the same infrastructure.

source

Configurations

In addition to the general configurations, error analysis can take the following specific configurations:

JET.JETAnalyzerConfigType

Every entry point of error analysis accepts any of the general configurations, together with the following configurations specific to error analysis.


  • mode::Symbol = :basic:
    Selects the error-analysis mode. Each mode reports problems according to its own definition of an error. JET provides the following modes:

    • mode = :basic: the default error-analysis mode. This mode reports common problems and is tuned for general Julia development, but it is not strict enough to guarantee error-free execution.
    • mode = :sound: the sound error-analysis mode. If this mode reports no errors, the analyzed code is guaranteed not to encounter a runtime error covered by JET's error model, assuming that the model and implementation are actually sound.
    • mode = :typo: the typo-detection mode. This mode is a subset of the default basic mode and reports a focused set of likely typo-related errors. These include undefined global, local, and static-parameter references; incompatible global assignments; and invalid field accesses or assignments. It can be useful for large or complex codebases where even the basic mode produces too many reports.
    Note

    You can also set up your own analysis using JET's AbstractAnalyzer framework.


  • ignore_missing_comparison::Bool = false:
    If true, JET widens any inferred call result that is exactly Union{Bool,Missing} to Any. This suppresses reports caused by branching on a possible missing result from a poorly inferred comparison operator such as ==. This is disabled by default because Union{Bool,Missing} can indicate imprecise inference or a case where missing should be handled explicitly. It can nevertheless reduce noise when precise input argument types are unavailable at the analysis entry point, as with report_package.

  • ignore_throws::Bool = false:
    If true, JET does not report errors from throw calls or exceptions that may propagate to callers. This is disabled by default, but is useful when analyzing package-level definitions where throw calls are often intentional, such as interface functions that throw errors by default. report_package enables this configuration by default to reduce noise from such intentional throws.

source
  • 1We used ignored_modules just for the sake of demonstration. To make it more idiomatic, we should initialize a as a typed vector, a = Int[], and then we will not get any problems from sum(a) even without the ignored_modules configuration.