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. But as opposed to other statically-compiled languages, Julia by design does not error nor warn anything even if it detects possible errors during its compilation process no matter how serious they are. In essence, Julia achieves highly generic and composable programming by delaying all the errors and warnings to the 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 can not 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's not straightforward to have such modern development experiences that a static language can typically offer, as like static type checking and rich IDE features.

JET is a trial to get the best of both worlds: can we have a sufficiently useful static checking without losing all the beauty of Julia's dynamism and composability? JET's approach is very different from "gradual typing", that is a common technique to bring static analysis into a dynamic language, as used for e.g. mypy for Python and TypeScript for JavaScript. Rather, JET's static analysis is powered by Julia's builtin type inference system, that is based on a technique called "abstract interpretation". This way JET can analyze just a normal Julia program and smartly detect possible errors statically, without requiring any additional setups like scattering type annotations just for the sake of analysis but preserving original polymorphism and composability of the program, as effectively as the Julia compiler can optimize your Julia program.

Quick Start

julia> using JET

Let's start with the simplest example: how JET can find anything wrong with sum("julia")? @report_call and report_call analyzes a given function call and report back possible problems. They can be used in a similar way as @code_typed and code_typed. Those 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:561
sum(a::String; kw::@Kwargs{}) @ Base ./reduce.jl:561
sum(f::typeof(identity), a::String) @ Base ./reduce.jl:532
sum(f::typeof(identity), a::String; kw::@Kwargs{}) @ Base ./reduce.jl:532
mapreduce(f::typeof(identity), op::typeof(Base.add_sum), itr::String) @ Base ./reduce.jl:307
mapreduce(f::typeof(identity), op::typeof(Base.add_sum), itr::String; kw::@Kwargs{}) @ Base ./reduce.jl:307
mapfoldl(f::typeof(identity), op::typeof(Base.add_sum), itr::String) @ Base ./reduce.jl:175
mapfoldl(f::typeof(identity), op::typeof(Base.add_sum), itr::String; init::Base._InitialValue) @ Base ./reduce.jl:175
mapfoldl_impl(f::typeof(identity), op::typeof(Base.add_sum), nt::Base._InitialValue, itr::String) @ Base ./reduce.jl:44
foldl_impl(op::Base.BottomRF{typeof(Base.add_sum)}, nt::Base._InitialValue, itr::String) @ Base ./reduce.jl:48
_foldl_impl(op::Base.BottomRF{typeof(Base.add_sum)}, init::Base._InitialValue, itr::String) @ Base ./reduce.jl:62
(::Base.BottomRF{typeof(Base.add_sum)})(acc::Char, x::Char) @ Base ./reduce.jl:86
add_sum(x::Char, y::Char) @ Base ./reduce.jl:24
│ 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:49
reduce_empty_iter(op::Base.BottomRF{typeof(Base.add_sum)}, itr::String) @ Base ./reduce.jl:380
reduce_empty_iter(op::Base.BottomRF{typeof(Base.add_sum)}, itr::String, ::Base.HasEltype) @ Base ./reduce.jl:381
reduce_empty(op::Base.BottomRF{typeof(Base.add_sum)}, ::Type{Char}) @ Base ./reduce.jl:357
reduce_empty(::typeof(Base.add_sum), ::Type{Char}) @ Base ./reduce.jl:350
reduce_empty(::typeof(+), ::Type{Char}) @ Base ./reduce.jl:343
│ 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:596
  +(::Integer, ::AbstractChar)
   @ Base char.jl:247
  +(::T, ::Integer) where T<:AbstractChar
   @ Base char.jl:237
  ...
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:310 zero(::Type{Dates.Time}) @ Dates /opt/hostedtoolcache/julia/nightly/x64/share/julia/stdlib/v1.11/Dates/src/types.jl:460 zero(::Type{Pkg.Resolve.VersionWeight}) @ Pkg /opt/hostedtoolcache/julia/nightly/x64/share/julia/stdlib/v1.11/Pkg/src/Resolve/versionweights.jl:15 ...

We should note that @report_call sum("julia") could detect both of those two different errors that can happen at runtime. This is because @report_call does a static analysis — it analyzes the function call in a way that does not rely on one instance of runtime execution, but rather it reasons about all the possible executions! This is one of the biggest advantages of static analysis because other alternatives to check software qualities like "testing" usually rely on some runtime execution and they can only cover a subset of all the possible executions.

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

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:980 sum(a::Vector{Any}; dims::Colon, kw::@Kwargs{}) @ Base ./reducedim.jl:980 _sum(a::Vector{Any}, ::Colon) @ Base ./reducedim.jl:984 _sum(a::Vector{Any}, ::Colon; kw::@Kwargs{}) @ Base ./reducedim.jl:984 _sum(f::typeof(identity), a::Vector{Any}, ::Colon) @ Base ./reducedim.jl:985 _sum(f::typeof(identity), a::Vector{Any}, ::Colon; kw::@Kwargs{}) @ Base ./reducedim.jl:985 mapreduce(f::typeof(identity), op::typeof(Base.add_sum), A::Vector{Any}) @ Base ./reducedim.jl:327 mapreduce(f::typeof(identity), op::typeof(Base.add_sum), A::Vector{Any}; dims::Colon, init::Base._InitialValue) @ Base ./reducedim.jl:327 _mapreduce_dim(f::typeof(identity), op::typeof(Base.add_sum), ::Base._InitialValue, A::Vector{Any}, ::Colon) @ Base ./reducedim.jl:335 _mapreduce(f::typeof(identity), op::typeof(Base.add_sum), ::IndexLinear, A::Vector{Any}) @ Base ./reduce.jl:429 mapreduce_empty_iter(f::typeof(identity), op::typeof(Base.add_sum), itr::Vector{Any}, ItrEltype::Base.HasEltype) @ Base ./reduce.jl:377 reduce_empty_iter(op::Base.MappingRF{typeof(identity), typeof(Base.add_sum)}, itr::Vector{Any}, ::Base.HasEltype) @ Base ./reduce.jl:381 reduce_empty(op::Base.MappingRF{typeof(identity), typeof(Base.add_sum)}, ::Type{Any}) @ Base ./reduce.jl:358 mapreduce_empty(::typeof(identity), op::typeof(Base.add_sum), T::Type{Any}) @ Base ./reduce.jl:369 reduce_empty(::typeof(Base.add_sum), ::Type{Any}) @ Base ./reduce.jl:350 reduce_empty(::typeof(+), ::Type{Any}) @ Base ./reduce.jl:343 zero(::Type{Any}) @ Base ./missing.jl:106 │ MethodError: no method matching zero(::Type{Any}): Base.throw(Base.MethodError(zero, tuple(Base.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, and so we just need to add a new bar(::AbstractString) definition. As for the second error, let's assume, for some reason, we're not interested in fixing it and we want to ignore errors that may happen within Base. Then we can use the target_modules configuration to limit the analysis scope to the current module context to ignore the possible error that may happen within sum(a)[1].

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

So far, we have used the default error analysis pass, which collects problems according to one specific (somewhat opinionated) definition of "errors" (see the JET.BasicPass for more details). JET offers other error reporting passes, including the "sound" error detection (JET.SoundPass) as well as the simpler "typo" detection pass (JET.TypoPass)[2]. They can be switched using the mode configuration:

julia> function myifelse(cond, a, b)
           if cond
               return a
           else
               return b
           end
       end
       
       # the default analysis pass doesn't report "non-boolean (T) used in boolean context" error
       # as far as there is a possibility when the condition "can" be bool (NOTE: Bool <: Integer)myifelse (generic function with 1 method)
julia> report_call(myifelse, (Integer, Int, Int)) # the sound analyzer doesn't permit such a case: it requires the type of a conditional value to be `Bool` strictlyNo errors detected
julia> 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 end # the default analysis pass will report both problems: # - `undefsum` is not defined # - `sum(a::Vector{Any})` can throw when `a` is emptystrange_sum (generic function with 1 method)
julia> @report_call strange_sum([]) # the typo detection pass will only report the "typo"═════ 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:980 sum(a::Vector{Any}; dims::Colon, kw::@Kwargs{}) @ Base ./reducedim.jl:980 _sum(a::Vector{Any}, ::Colon) @ Base ./reducedim.jl:984 _sum(a::Vector{Any}, ::Colon; kw::@Kwargs{}) @ Base ./reducedim.jl:984 _sum(f::typeof(identity), a::Vector{Any}, ::Colon) @ Base ./reducedim.jl:985 _sum(f::typeof(identity), a::Vector{Any}, ::Colon; kw::@Kwargs{}) @ Base ./reducedim.jl:985 mapreduce(f::typeof(identity), op::typeof(Base.add_sum), A::Vector{Any}) @ Base ./reducedim.jl:327 mapreduce(f::typeof(identity), op::typeof(Base.add_sum), A::Vector{Any}; dims::Colon, init::Base._InitialValue) @ Base ./reducedim.jl:327 _mapreduce_dim(f::typeof(identity), op::typeof(Base.add_sum), ::Base._InitialValue, A::Vector{Any}, ::Colon) @ Base ./reducedim.jl:335 _mapreduce(f::typeof(identity), op::typeof(Base.add_sum), ::IndexLinear, A::Vector{Any}) @ Base ./reduce.jl:429 mapreduce_empty_iter(f::typeof(identity), op::typeof(Base.add_sum), itr::Vector{Any}, ItrEltype::Base.HasEltype) @ Base ./reduce.jl:377 reduce_empty_iter(op::Base.MappingRF{typeof(identity), typeof(Base.add_sum)}, itr::Vector{Any}, ::Base.HasEltype) @ Base ./reduce.jl:381 reduce_empty(op::Base.MappingRF{typeof(identity), typeof(Base.add_sum)}, ::Type{Any}) @ Base ./reduce.jl:358 mapreduce_empty(::typeof(identity), op::typeof(Base.add_sum), T::Type{Any}) @ Base ./reduce.jl:369 reduce_empty(::typeof(Base.add_sum), ::Type{Any}) @ Base ./reduce.jl:350 reduce_empty(::typeof(+), ::Type{Any}) @ Base ./reduce.jl:343 zero(::Type{Any}) @ Base ./missing.jl:106 │ MethodError: no method matching zero(::Type{Any}): Base.throw(Base.MethodError(zero, tuple(Base.Any)::Tuple{DataType})::MethodError) └────────────────────
julia> @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 Test standard library's unit-testing infrastructure:

julia> @test_call target_modules=(@__MODULE__,) foo("1 2 3")Test Passed
julia> using Test # we can get the nice summery using `@testset` !
julia> @testset "JET testset" begin @test_call target_modules=(@__MODULE__,) 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 # of course other `Test` macros can be used in the same place endJET testset: JET-test failed at /home/runner/work/JET.jl/JET.jl/src/JET.jl:1132 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 2.6s ERROR: Some tests did not pass: 2 passed, 1 failed, 0 errored, 1 broken.

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

Lastly, let's see the example that demonstrates JET can analyze a "top-level" program. The top-level analysis should be considered as a somewhat experimental feature, and at this moment 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.014 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.132 sec)
═════ 5 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:402
<(x::String, y::Int64) @ Base ./operators.jl:353
│ 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: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.jl:49
│ type Ty has no field fdl: Base.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
└────────────────────

Errors 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 normal MethodErrors, this happens if a function is being called without a method matching 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]:2 │ no matching method found `f(::Float64)`: Main.f(x::Float64) └────────────────────

How to fix

This error indicates some kind of type error in your code. You fix it like you would fix 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, if the compiler infers x to be of type Union{A, B}, and then a function f(x) is called which would lead to a MethodError if x is a A, this error would occur.

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

Example

Minimal example:

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

There are different possibilities to address this kind of error. Let's take the pos_after_tab example:

If you actually could expect p to legitimately 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 always should return an Int for valid input, you can add a typeassert in the function to assert that the return value of findfirst must be, say, an Integer. Then, the compiler will know that if the typeassert passes, the value returned by findfirst cannot be nothing (and hence in this case must be 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 typeassert if findfirst returns nothing, but JET will no longer detect it as an error, because the programmer, by adding the typeassert, explicitly acknowledge that the compiler's inference may not be precise enough, and helps the compiler.

Note that adding a typeassert also improves code quality:

  • The programmer's intent to never observe nothing is communicated clearly
  • After the typeassert 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, in 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]:7 │ no matching method found `+(::Nothing, ::Int64)` (1/2 union split): Main.:+((x::Main.Foo).x::Union{Nothing, Int64}, 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 expression x.x on the like with the if statement in this case 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 in as an argument to the offending function.

type T has no field F

Description

This error occurs when Core.getfield is (indirectly) called with a nonexisting and hardcoded field name. For example, if an object have a field called vec and you type it 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]:2 getproperty(x::Main.Foo, f::Symbol) @ Base ./Base.jl:49 │ type Foo has no field my_feild: Base.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 a function attempts to access a container out of bounds, both the container length and the index value must be known at compiletime. 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:31 │ BoundsError: attempt to access Tuple{Int64, Int64, Int64} at index [4]: Base.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 hardcoded, 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, and so 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's not known at compile time whether it throws, and therefore, 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]:3 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 to find type-level errors and returns back detected problems.

The general configurations and the error analysis specific configurations can be specified as a keyword argument.

See the documentation of the error analysis for more details.

source

Top-level Entry Points

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

Note that JET will analyze your top-level program "half-statically": JET will selectively interpret and load "definitions" (like a function or struct definition) and try to simulate Julia's top-level code execution process. While it tries to avoid executing any other parts of code like function calls and analyzes them based on abstract interpretation instead (and this is a part where JET statically analyzes your code). If you're interested in how JET selects "top-level definitions", please see JET.virtual_process.

Warning

Because JET will interpret "definitions" in your code, that part of top-level analysis certainly runs your code. So we should note that JET can cause some side effects from your code; for example, JET will try to expand all the macros used in your code, and so the side effects involved with macro expansions will also happen in JET's analysis process.

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

Analyzes file to find type-level errors and returns back detected problems.

This function looks for .JET.toml configuration file in the directory of file, and searches upward in the file tree until a .JET.toml is (or isn't) found. When found, the configurations specified in the file are applied. See JET's configuration file specification for more details.

The general configurations and the error analysis specific configurations can be specified as a keyword argument, and if given, they are preferred over the configurations specified by a .JET.toml configuration file.

Tip

When you want to analyze your package but no files that actually use its functions are available, the analyze_from_definitions option may be useful since it allows JET to analyze methods based on their declared signatures. For example, JET can analyze JET itself in 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 the toplevel_logger configuration with the default logging level by default. You can still explicitly specify and configure it:

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.watch_fileFunction
watch_file(file::AbstractString; jetconfigs...)

Watches file and keeps re-triggering analysis with report_file on code update. JET will try to analyze all the included files reachable from file, and it will re-trigger analysis if there is code update detected in any of the included files.

This function internally uses Revise.jl to track code updates. Revise also offers possibilities to track changes in files that are not directly analyzed by JET, or even changes in Base files. See watch configurations for more details.

Warning

This interface is very experimental and likely to subject to change or removal without notice.

See also report_file.

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

Analyzes package in the same way as report_file and returns back type-level errors with the special default configurations, which are especially tuned for analyzing a package (see below for details). The package argument can be either a Module or a AbstractString. In the latter case it must be the name of a package in your current environment.

The error analysis performed by this function is configured as follows by default:

  • analyze_from_definitions = true: This allows JET to start analysis without top-level call sites. This is useful for analyzing a package since a package itself usually only contains definitions of types and methods but not their usages (i.e. call sites).
  • concretization_patterns = [:(x_)]: Concretizes every top-level code in a given package. The concretizations are generally preferred for successful analysis as far as they can be performed cheaply. In most cases it is indeed cheap to interpret and concretize top-level code written in a package since it usually only defines types and methods.
  • ignore_missing_comparison = true: JET ignores the possibility of a poorly-inferred comparison operator call (e.g. ==) returning missing. This is useful because report_package often relies on poor input argument type information at the beginning of analysis, leading to noisy error reports from branching on the potential missing return value of such a comparison operator call. If a target package needs to handle missing, this configuration shuold be turned off since it hides the possibility of errors that may actually at runtime.

See ToplevelConfig and JETAnalyzer for more details.

Still the general configurations and the error analysis specific configurations can be specified as a keyword argument, and if given, they are preferred over the default configurations described above.


report_package(; jetconfigs...) -> JETToplevelResult

Like above but analyzes the package of the current project.

See also report_file.

source
JET.report_textFunction
report_text(text::AbstractString; jetconfigs...) -> JETToplevelResult
report_text(text::AbstractString, filename::AbstractString; jetconfigs...) -> JETToplevelResult

Analyzes top-level text and returns back type-level errors.

source

Test Integration

JET also exports entries that are fully integrated with Test standard library's unit-testing infrastructure. It can be used in your test suite to assert 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 tests that the function call f(args...) is free from problems that @report_call can detect. Returns a Pass result if the test is successful, a Fail result if any problems are detected, or an Error result if the test encounters an unexpected error. When the test Fails, abstract call stack to each problem location will be printed to stdout.

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

As with @report_call, the general configurations and the error analysis specific configurations can be specified as an optional argument:

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 sin(n)
           else
               return cos(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 ═════
  ┌ @ none:2 goto %4 if not cond
  │ non-boolean (Any) used in boolean context: goto %4 if not cond
  └──────────

ERROR: There was an error during testing

@test_call is fully integrated with Test standard library's unit-testing infrastructure. This means that the result of @test_call will be included in a final @testset summary and it supports skip and broken annotations, just like the @test macro:

julia> using JET, Test

# Julia can't propagate the type constraint `ref[]::Number` to `sin(ref[])`, JET will report `NoMethodError`
julia> f(ref) = isa(ref[], Number) ? sin(ref[]) : nothing;

# we can make it type-stable if we extract `ref[]` into a local variable `x`
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)             # fail
           @test_call broken=true f(ref) # annotated as broken, thus still "pass"
       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. Except that it takes a type signature rather than a call expression, this function works in the same way as @test_call.

source
JET.test_packageFunction
test_package(package::Module; jetconfigs...)
test_package(package::AbstractString; jetconfigs...)
test_package(; jetconfigs...)

Runs report_package and tests that there are no problems detected.

As with report_package, the general configurations and the error analysis specific configurations can be specified as an optional argument.

Like @test_call, test_package is fully integrated with the Test standard library. See @test_call for the details.

julia> @testset "test_package" begin
           test_package("Example"; toplevel_logger=nothing)
       end;
Test Summary: | Pass  Total  Time
test_package  |    1      1  0.0s
source

Configurations

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

JET.JETAnalyzerType

Every entry point of error analysis can accept any of the general configurations as well as the following additional configurations that are specific to the error analysis.


  • mode::Symbol = :basic:
    Switches the error analysis pass. Each analysis pass reports errors according to their own "error" definition. JET by default offers the following modes:

    • mode = :basic: the default error analysis pass. This analysis pass is tuned to be useful for general Julia development by reporting common problems, but also note that it is not enough strict to guarantee that your program never throws runtime errors.
      See BasicPass for more details.
    • mode = :sound: the sound error analysis pass. If this pass doesn't report any errors, then your program is assured to run without any runtime errors (unless JET's error definition is not accurate and/or there is an implementation flaw).
      See SoundPass for more details.
    • mode = :typo: a typo detection pass A simple analysis pass to detect "typo"s in your program. This analysis pass is essentially a subset of the default basic pass (BasicPass), and it only reports undefined global reference and undefined field access. This might be useful especially for a very complex code base, because even the basic pass tends to be too noisy (spammed with too many errors) for such a case.
      See TypoPass for more details.
    Note

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


  • ignore_missing_comparison::Bool = false:
    If true, JET will ignores the possibility of a poorly-inferred comparison operator call (e.g. ==) returning missing in order to hide the error reports from branching on the potential missing return value of such a comparison operator call. This is turned off by default, because a comparison call results in a Union{Bool,Missing} possibility, it likely signifies an inferrability issue or the missing possibility should be handled someway. But this is useful to reduce the noisy error reports in the situations where specific input arguments type is not available at the beginning of the analysis like report_package.

source
  • 1We used target_modules just for the sake of demonstration. To make it more idiomatic, we should initialize a as typed vector a = Int[], and then we won't get any problem from sum(a) even without the target_modules configuration.
  • 2JET offers the framework to define your own abstract interpretation-based analysis. See AbstractAnalyzer-Framework if interested.