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 JETLet'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 detectedjulia> # 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 Passedjulia> using Testjulia> # 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} endjulia> # 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
nothingis communicated clearly. - After the type assertion passes,
pis inferred to beIntinstead 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} endjulia> 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:
Xis misspelled. If so, correct the typo.Xexists 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 endjulia> 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 detectedjulia> @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 detectedjulia> @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_call — Macro
@report_call [jetconfigs...] f(args...)Evaluates the function and its arguments, determines their types, and calls report_call with the resulting function and argument-type signature. This macro works similarly to the @code_typed macro.
The general configurations and error-analysis-specific configurations can be supplied as optional leading configuration arguments.
JET.report_call — Function
report_call(f, [types]; jetconfigs...) -> JETCallResult
report_call(tt::Type{<:Tuple}; jetconfigs...) -> JETCallResult
report_call(mi::Core.MethodInstance; jetconfigs...) -> JETCallResultAnalyzes 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.
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.
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_file — Function
report_file(file::AbstractString; jetconfigs...) -> JETToplevelResultAnalyzes 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.
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.
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 configurationsSee JET's top-level analysis configurations for more details.
JET.report_package — Function
report_package(package::Module; jetconfigs...) -> JETToplevelResultAnalyzes 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 exactlyUnion{Bool,Missing}toAny. This reduces noise becausereport_packageoften begins analysis with imprecise argument types. Disable this configuration when the package intentionally handlesmissing, because it can hide errors that may occur at runtime.ignore_throws = true: JET does not report errors fromthrowcalls 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.
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 ═════
...JET.report_text — Function
report_text(text::AbstractString; jetconfigs...) -> JETToplevelResult
report_text(text::AbstractString, filename::AbstractString;
jetconfigs...) -> JETToplevelResultAnalyzes the top-level code in text and returns a JETToplevelResult containing the detected type-level problems.
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_call — Macro
@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.JET.test_call — Function
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.
JET.test_file — Function
test_file(file::AbstractString; broken::Bool = false, skip::Bool = false, jetconfigs...)Runs report_file and tests that there are no problems detected.
As with report_file, the general configurations and error-analysis-specific configurations can be supplied as keyword arguments.
Like @test_call, test_file integrates with the Test standard library. See @test_call for details.
JET.test_package — Function
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.0sJET.test_text — Function
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.
JETAnalyzer
JET.JETAnalyzer — Type
abstract type JETAnalyzer <: ToplevelAbstractAnalyzer endJET'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) constructsBasicJETAnalyzer, 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 = :soundconstructsSoundJETAnalyzer, 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 = :typoconstructsTypoJETAnalyzer, 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.
Configurations
In addition to the general configurations, error analysis can take the following specific configurations:
JET.JETAnalyzerConfig — Type
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
AbstractAnalyzerframework.
ignore_missing_comparison::Bool = false:
Iftrue, JET widens any inferred call result that is exactlyUnion{Bool,Missing}toAny. This suppresses reports caused by branching on a possiblemissingresult from a poorly inferred comparison operator such as==. This is disabled by default becauseUnion{Bool,Missing}can indicate imprecise inference or a case wheremissingshould be handled explicitly. It can nevertheless reduce noise when precise input argument types are unavailable at the analysis entry point, as withreport_package.
ignore_throws::Bool = false:
Iftrue, JET does not report errors fromthrowcalls or exceptions that may propagate to callers. This is disabled by default, but is useful when analyzing package-level definitions wherethrowcalls are often intentional, such as interface functions that throw errors by default.report_packageenables this configuration by default to reduce noise from such intentional throws.
- 1We used
ignored_modulesjust for the sake of demonstration. To make it more idiomatic, we should initializeaas a typed vector,a = Int[], and then we will not get any problems fromsum(a)even without theignored_modulesconfiguration.