JET.VSCode.VSCodeConfigType

Configurations for the VSCode integration. These configurations are active only when used in the integrated Julia REPL.


  • vscode_console_output::Union{Nothing,IO} = stdout
    JET will show analysis result in VSCode's "PROBLEMS" pane and inline annotations. If vscode_console_output::IO is specified, JET will also print the result into the specified output stream in addition to showing the result in the integrated views. When nothing, the result will be only shown in the integrated views.

JET.VSCode.vscode_diagnostics_orderMethod
vscode_diagnostics_order(analyzer::AbstractAnalyzer) -> Bool

If true (default) a diagnostic will be reported at entry site. Otherwise it's reported at error point.

JET.JETModule

JET.jl

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

Note

JET is tightly coupled to the Julia compiler, and so each JET release supports a limited range of Julia versions. See the Project.toml file for the range of supported Julia versions. The Julia package manager should install a version of JET compatible with the Julia version you are running. If you want to use JET on unreleased version of Julia where compatibility with JET is yet unknown, clone this git repository and dev it, such that Julia compatibility is ignored.

Note

Also note that the tight coupling of JET and the Julia compiler means that JET results can vary depending on your Julia version. In general, the newer your Julia version is, the more accurately and quickly you can expect JET to analyze your code, assuming the Julia compiler keeps evolving all the time from now on.

Quickstart

See more commands, options and explanations in the documentation.

Installation

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

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

julia> using JET

Detect type instability with @report_opt

Type instabilities can be detected in function calls using the @report_opt macro, which works similar to the @code_warntype macro. Note that, because JET relies on Julia's type inference, if a chain of inference is broken due to dynamic dispatch, then all downstream function calls will be unknown to the compiler, and so JET cannot analyze them.

julia> @report_opt foldl(+, Any[]; init=0)
═════ 2 possible errors found ═════
┌ kwcall(::@NamedTuple{init::Int64}, ::typeof(foldl), op::typeof(+), itr::Vector{Any}) @ Base ./reduce.jl:198
│┌ foldl(op::typeof(+), itr::Vector{Any}; kw::@Kwargs{init::Int64}) @ Base ./reduce.jl:198
││┌ kwcall(::@NamedTuple{init::Int64}, ::typeof(mapfoldl), f::typeof(identity), op::typeof(+), itr::Vector{Any}) @ Base ./reduce.jl:175
│││┌ mapfoldl(f::typeof(identity), op::typeof(+), itr::Vector{Any}; init::Int64) @ Base ./reduce.jl:175
││││┌ mapfoldl_impl(f::typeof(identity), op::typeof(+), nt::Int64, itr::Vector{Any}) @ Base ./reduce.jl:44
│││││┌ foldl_impl(op::Base.BottomRF{typeof(+)}, nt::Int64, itr::Vector{Any}) @ Base ./reduce.jl:48
││││││┌ _foldl_impl(op::Base.BottomRF{typeof(+)}, init::Int64, itr::Vector{Any}) @ Base ./reduce.jl:58
│││││││┌ (::Base.BottomRF{typeof(+)})(acc::Int64, x::Any) @ Base ./reduce.jl:86
││││││││ runtime dispatch detected: +(acc::Int64, x::Any)::Any
│││││││└────────────────────
││││││┌ _foldl_impl(op::Base.BottomRF{typeof(+)}, init::Int64, itr::Vector{Any}) @ Base ./reduce.jl:62
│││││││┌ (::Base.BottomRF{typeof(+)})(acc::Any, x::Any) @ Base ./reduce.jl:86
││││││││ runtime dispatch detected: +(acc::Any, x::Any)::Any
│││││││└────────────────────

Detect type errors with @report_call

This works best on type stable code, so use @report_opt liberally before using @report_call.

julia> @report_call foldl(+, Char[])
═════ 2 possible errors found ═════
┌ foldl(op::typeof(+), itr::Vector{Char}) @ Base ./reduce.jl:198
│┌ foldl(op::typeof(+), itr::Vector{Char}; kw::@Kwargs{}) @ Base ./reduce.jl:198
││┌ mapfoldl(f::typeof(identity), op::typeof(+), itr::Vector{Char}) @ Base ./reduce.jl:175
│││┌ mapfoldl(f::typeof(identity), op::typeof(+), itr::Vector{Char}; init::Base._InitialValue) @ Base ./reduce.jl:175
││││┌ mapfoldl_impl(f::typeof(identity), op::typeof(+), nt::Base._InitialValue, itr::Vector{Char}) @ Base ./reduce.jl:44
│││││┌ foldl_impl(op::Base.BottomRF{typeof(+)}, nt::Base._InitialValue, itr::Vector{Char}) @ Base ./reduce.jl:48
││││││┌ _foldl_impl(op::Base.BottomRF{typeof(+)}, init::Base._InitialValue, itr::Vector{Char}) @ Base ./reduce.jl:62
│││││││┌ (::Base.BottomRF{typeof(+)})(acc::Char, x::Char) @ Base ./reduce.jl:86
││││││││ no matching method found `+(::Char, ::Char)`: (op::Base.BottomRF{typeof(+)}).rf::typeof(+)(acc::Char, x::Char)
│││││││└────────────────────
│││││┌ foldl_impl(op::Base.BottomRF{typeof(+)}, nt::Base._InitialValue, itr::Vector{Char}) @ Base ./reduce.jl:49
││││││┌ reduce_empty_iter(op::Base.BottomRF{typeof(+)}, itr::Vector{Char}) @ Base ./reduce.jl:383
│││││││┌ reduce_empty_iter(op::Base.BottomRF{typeof(+)}, itr::Vector{Char}, ::Base.HasEltype) @ Base ./reduce.jl:384
││││││││┌ reduce_empty(op::Base.BottomRF{typeof(+)}, ::Type{Char}) @ Base ./reduce.jl:360
│││││││││┌ reduce_empty(::typeof(+), ::Type{Char}) @ Base ./reduce.jl:343
││││││││││ no matching method found `zero(::Type{Char})`: zero(T::Type{Char})
│││││││││└────────────────────

Analyze packages with report_package

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

julia> using Pkg; Pkg.activate(; temp=true, io=devnull); Pkg.add("AbstractTrees"; io=devnull);

julia> Pkg.status()
Status `/private/var/folders/xh/6zzly9vx71v05_y67nm_s9_c0000gn/T/jl_h07K2m/Project.toml`
  [1520ce14] AbstractTrees v0.4.4

julia> report_package("AbstractTrees")
[ some output elided ]
═════ 7 possible errors found ═════
┌ isroot(root::Any, x::Any) @ AbstractTrees ~/.julia/packages/AbstractTrees/EUx8s/src/base.jl:102
│ no matching method found `parent(::Any, ::Any)`: AbstractTrees.parent(root::Any, x::Any)
└────────────────────
┌ AbstractTrees.IndexNode(tree::Any) @ AbstractTrees ~/.julia/packages/AbstractTrees/EUx8s/src/indexing.jl:117
│ no matching method found `rootindex(::Any)`: rootindex(tree::Any)
└────────────────────
┌ parent(idx::AbstractTrees.IndexNode) @ AbstractTrees ~/.julia/packages/AbstractTrees/EUx8s/src/indexing.jl:127
│ no matching method found `parentindex(::Any, ::Any)`: pidx = parentindex((idx::AbstractTrees.IndexNode).tree::Any, (idx::AbstractTrees.IndexNode).index::Any)
└────────────────────
┌ nextsibling(idx::AbstractTrees.IndexNode) @ AbstractTrees ~/.julia/packages/AbstractTrees/EUx8s/src/indexing.jl:132
│ no matching method found `nextsiblingindex(::Any, ::Any)`: sidx = nextsiblingindex((idx::AbstractTrees.IndexNode).tree::Any, (idx::AbstractTrees.IndexNode).index::Any)
└────────────────────
┌ prevsibling(idx::AbstractTrees.IndexNode) @ AbstractTrees ~/.julia/packages/AbstractTrees/EUx8s/src/indexing.jl:137
│ no matching method found `prevsiblingindex(::Any, ::Any)`: sidx = prevsiblingindex((idx::AbstractTrees.IndexNode).tree::Any, (idx::AbstractTrees.IndexNode).index::Any)
└────────────────────
┌ prevsibling(csr::AbstractTrees.IndexedCursor) @ AbstractTrees ~/.julia/packages/AbstractTrees/EUx8s/src/cursors.jl:234
│ no matching method found `getindex(::Nothing, ::Int64)` (1/2 union split): (AbstractTrees.parent(csr::AbstractTrees.IndexedCursor)::Union{Nothing, AbstractTrees.IndexedCursor})[idx::Int64]
└────────────────────
┌ (::AbstractTrees.var"#17#18")(n::Any) @ AbstractTrees ~/.julia/packages/AbstractTrees/EUx8s/src/iteration.jl:323
│ no matching method found `parent(::Any, ::Any)`: AbstractTrees.parent(getfield(#self#::AbstractTrees.var"#17#18", :tree)::Any, n::Any)
└────────────────────

Limitations

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

JET integrates with SnoopCompile, and you can sometimes use SnoopCompile to collect the data to perform more comprehensive analyses. SnoopCompile's limitation is that it only collects data for calls that have not been previously inferred, so you must perform this type of analysis in a fresh session.

See SnoopCompile's JET-integration documentation for further details.

Acknowledgement

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

JET.JET_METHOD_TABLEConstant
JET_METHOD_TABLE

This JET-specific method table keeps method definition overrides, that allow us to cut off false positive errors, while simulating the original semantics reasonably. This works as a temporal patch, and ideally we want to port it back to the Julia base or a package, or improve the accuracy of base abstract interpretation analysis.

JET.AbstractAnalyzerType
abstract type AbstractAnalyzer <: AbstractInterpreter end

An interface type of analyzers that are built on top of JET's analyzer framework.

When a new type NewAnalyzer implements the AbstractAnalyzer interface, it should be declared as subtype of AbstractAnalyzer, and is expected to the following interfaces:


  1. AnalyzerState(analyzer::NewAnalyzer) -> AnalyzerState:
    Returns the AnalyzerState for analyzer::NewAnalyzer.

  1. AbstractAnalyzer(analyzer::NewAnalyzer, state::AnalyzerState) -> NewAnalyzer:
    Constructs an new NewAnalyzer instance in the middle of JET's top-level analysis or abstract interpretation, given the previous analyzer::NewAnalyzer and state::AnalyzerState.

  1. ReportPass(analyzer::NewAnalyzer) -> ReportPass:
    Returns ReportPass used for analyzer::NewAnalyzer.

  1. AnalysisCache(analyzer::NewAnalyzer) -> analysis_cache::AnalysisCache:
    Returns code cache used for analyzer::NewAnalyzer.

See also AnalyzerState, ReportPass and AnalysisCache.

Example

JET.jl defines its default error analyzer JETAnalyzer <: AbstractAnalyzer as the following (modified a bit for the sake of simplicity):

# the default error analyzer for JET.jl
struct JETAnalyzer{RP<:ReportPass} <: AbstractAnalyzer
    state::AnalyzerState
    analysis_cache::AnalysisCache
    report_pass::RP
end

# AbstractAnalyzer API requirements
AnalyzerState(analyzer::JETAnalyzer) = analyzer.state
AbstractAnalyzer(analyzer::JETAnalyzer, state::AnalyzerState) = JETAnalyzer(ReportPass(analyzer), state)
ReportPass(analyzer::JETAnalyzer) = analyzer.report_pass
AnalysisCache(analyzer::JETAnalyzer) = analyzer.analysis_cache
JET.AbstractBuiltinErrorReportType
AbstractBuiltinErrorReport

Represents errors caused by builtin-function calls. Technically they're defined as those error points that can be caught within Core.Compiler.builtin_tfunction.

JET.AbstractGlobalType
mutable struct AbstractGlobal
    t::Any     # analyzed type
    isconst::Bool # is this abstract global variable declarared as constant or not
end

Wraps a global variable whose type is analyzed by abstract interpretation. AbstractGlobal object will be actually evaluated into the context module, and a later analysis may refer to or alter its type on future load and store operations.

Note

The type of the wrapped global variable will be propagated only when in a toplevel frame, and thus we don't care about the analysis cache invalidation on a refinement of the wrapped global variable, since JET doesn't cache the toplevel frame.

JET.AnalysisCacheType
AnalysisCache

JET's internal representation of a global analysis cache.

JET.AnalysisCacheMethod
AnalysisCache(analyzer::AbstractAnalyzer) -> analysis_cache::AnalysisCache

Returns AnalysisCache for this analyzer::AbstractAnalyzer. AbstractAnalyzer instances can share the same cache if they perform the same analysis, otherwise their cache should be separated.

JET.AnalyzerStateType
mutable struct AnalyzerState
    ...
end

The mutable object that holds various states that are consumed by all AbstractAnalyzers.


AnalyzerState(analyzer::AbstractAnalyzer) -> AnalyzerState

If NewAnalyzer implements the AbstractAnalyzer interface, NewAnalyzer should implement this AnalyzerState(analyzer::NewAnalyzer) -> AnalyzerState interface.

A new AnalyzerState is supposed to be constructed using the general configurations passed as keyword arguments jetconfigs of the NewAnalyzer(; jetconfigs...) constructor, and the constructed AnalyzerState is usually kept within NewAnalyzer itself:

function NewAnalyzer(world::UInt=Base.get_world_counter(); jetconfigs...)
    ...
    state = AnalyzerState(world; jetconfigs...)
    return NewAnalyzer(..., state)
end
AnalyzerState(analyzer::NewAnalyzer) = analyzer.state
JET.BasicPassType

The basic error analysis pass. This is used by default.

JET.CachedAnalysisResultType
CachedAnalysisResult

AnalysisResult is transformed into CachedAnalysisResult when it is cached into a global cache maintained by AbstractAnalyzer. That means, codeinf::CodeInstance = Core.Compiler.code_cache(analyzer::AbstractAnalyzer)[mi::MethodInstance]) is expected to have its field codeinf.inferred::CachedAnalysisResult.

InferenceErrorReports found within already-analyzed result::InferenceResult can be accessed with get_cached_reports(analyzer, result).

JET.ConcreteInterpreterType
ConcreteInterpreter

The trait to inject code into JuliaInterpreter's interpretation process; JET.jl overloads:

  • JuliaInterpreter.step_expr! to add error report pass for module usage expressions and support package analysis
  • JuliaInterpreter.evaluate_call_recurse! to special case include calls
  • JuliaInterpreter.handle_err to wrap an error happened during interpretation into ActualErrorWrapped
JET.InferenceErrorReportType
abstract type InferenceErrorReport end

An interface type of error reports collected by JET's abstract interpretation based analysis. All InferenceErrorReports have the following fields, which explains where and how this error is reported:

Note that some InferenceErrorReport may have additional fields other than vst and sig to explain why they are reported.

JET.InferenceErrorReportMethod
InferenceErrorReport

In order for Report <: InferenceErrorReport to implement the interface, it should satisfy the following requirements:

Report <: InferenceErrorReport is supposed to be constructed using the following constructor

Report(::AbstractAnalyzer, state, spec_args...) -> Report

where state can be either of:

  • state::Tuple{Union{Core.Compiler.InferenceState, Core.Compiler.OptimizationState}, Int64}: a state with the current program counter specified
  • state::InferenceState: a state with the current program counter set to state.currpc
  • state::InferenceResult: a state with the current program counter unknown
  • state::MethodInstance: a state with the current program counter unknown

See also: @jetreport, VirtualStackTrace, VirtualFrame

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.

JET.JETCallResultType
res::JETCallResult

Represents the result of JET's analysis on a function call.

  • res.result::InferenceResult: the result of this analysis
  • res.analyzer::AbstractAnalyzer: AbstractAnalyzer used for this analysis
  • res.source::AbstractString: the identity key of this analysis
  • res.jetconfigs: configurations used for this analysis

JETCallResult implements show methods for each different frontend. An appropriate show method will be automatically chosen and render the analysis result.

JET.JETToplevelResultType
res::JETToplevelResult

Represents the result of JET's analysis on a top-level script.

  • res.analyzer::AbstractAnalyzer: AbstractAnalyzer used for this analysis
  • res.res::VirtualProcessResult: VirtualProcessResult collected from this analysis
  • res.source::AbstractString: the identity key of this analysis
  • res.jetconfigs: configurations used for this analysis

JETToplevelResult implements show methods for each different frontend. An appropriate show method will be automatically chosen and render the analysis result.

JET.OptAnalyzerType

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


  • skip_noncompileable_calls::Bool = true:
    Julia's runtime dispatch is "powerful" because it can always compile code with concrete runtime arguments so that a "kernel" function runs very effectively even if it's called from a type-instable call site. This means, we (really) often accept that some parts of our code are not inferred statically, and rather we want to just rely on information that is only available at runtime. To model this programming style, the optimization analyzer by default does NOT report any optimization failures or runtime dispatches detected within non-concrete calls (more correctly, "non-compileable" calls are ignored: see also the note below). We can turn off this skip_noncompileable_calls configuration to get type-instabilities within those calls.

    # the following examples are adapted from https://docs.julialang.org/en/v1/manual/performance-tips/#kernel-functions
    julia> function fill_twos!(a)
               for i = eachindex(a)
                   a[i] = 2
               end
           end;
    
    julia> function strange_twos(n)
               a = Vector{rand(Bool) ? Int64 : Float64}(undef, n)
               fill_twos!(a)
               return a
           end;
    
    # by default, only type-instabilities within concrete call (i.e. `strange_twos(3)`) are reported
    # and those within non-concrete calls (`fill_twos!(a)`) are not reported
    julia> @report_opt strange_twos(3)
    ═════ 2 possible errors found ═════
    ┌ strange_twos(n::Int64) @ Main ./REPL[23]:2
    │ runtime dispatch detected: %33::Type{Vector{_A}} where _A(undef, n::Int64)::Vector
    └────────────────────
    ┌ strange_twos(n::Int64) @ Main ./REPL[23]:3
    │ runtime dispatch detected: fill_twos!(%34::Vector)::Any
    └────────────────────
    
    # we can get reports from non-concrete calls with `skip_noncompileable_calls=false`
    julia> @report_opt skip_noncompileable_calls=false strange_twos(3)
    ┌ strange_twos(n::Int64) @ Main ./REPL[23]:3
    │┌ fill_twos!(a::Vector) @ Main ./REPL[22]:3
    ││┌ setindex!(A::Vector, x::Int64, i1::Int64) @ Base ./array.jl:1014
    │││ runtime dispatch detected: convert(%5::Any, x::Int64)::Any
    ││└────────────────────
    │┌ fill_twos!(a::Vector) @ Main ./REPL[22]:3
    ││ runtime dispatch detected: ((a::Vector)[%13::Int64] = 2::Any)
    │└────────────────────
    ┌ strange_twos(n::Int64) @ Main ./REPL[23]:2
    │ runtime dispatch detected: %33::Type{Vector{_A}} where _A(undef, n::Int64)::Vector
    └────────────────────
    ┌ strange_twos(n::Int64) @ Main ./REPL[23]:3
    │ runtime dispatch detected: fill_twos!(%34::Vector)::Any
    └────────────────────
    Non-compileable calls

    Julia runtime system sometimes generate and execute native code of an abstract call. More technically, when some of call arguments are annotated as @nospecialize, Julia compiles the call even if those @nospecialized arguments aren't fully concrete. skip_noncompileable_calls = true also respects this behavior, i.e. doesn't skip compileable abstract calls:

    julia> function maybesin(x)
               if isa(x, Number)
                   return sin(x)
               else
                   return 0
               end
           end;
    
    julia> report_opt((Vector{Any},)) do xs
               for x in xs
                   # This `maybesin` call is dynamically dispatched since `maybesin(::Any)`
                   # is not compileable. Therefore, JET by default will only report the
                   # runtime dispatch of `maybesin` while it will not report the runtime
                   # dispatch within `maybesin(::Any)`.
                   s = maybesin(x)
                   s !== 0 && return s
               end
           end
    ═════ 1 possible error found ═════
    ┌ (::var"#3#4")(xs::Vector{Any}) @ Main ./REPL[3]:7
    │ runtime dispatch detected: maybesin(%19::Any)::Any
    └────────────────────
    
    julia> function maybesin(@nospecialize x) # mark `x` with `@nospecialize`
               if isa(x, Number)
                   return sin(x)
               else
                   return 0
               end
           end;
    
    julia> report_opt((Vector{Any},)) do xs
               for x in xs
                   # Now `maybesin` is marked with `@nospecialize` allowing `maybesin(::Any)`
                   # to be resolved statically and compiled. Thus JET will not report the
                   # runtime dispatch of `maybesin(::Any)`, although it now reports the
                   # runtime dispatch _within_ `maybesin(::Any)`.
                   s = maybesin(x)
                   s !== 0 && return s
               end
           end
    ═════ 1 possible error found ═════
    ┌ (::var"#5#6")(xs::Vector{Any}) @ Main ./REPL[5]:7
    │┌ maybesin(x::Any) @ Main ./REPL[4]:3
    ││ runtime dispatch detected: sin(%3::Number)::Any
    │└────────────────────

  • function_filter = @nospecialize(f)->true:
    A predicate which takes a function object and returns false to skip runtime dispatch analysis on calls of the function. This configuration is particularly useful when your program uses a function that is intentionally designed to use runtime dispatch.

    # ignore `Core.Compiler.widenconst` calls (since it's designed to be runtime-dispatched):
    julia> function_filter(@nospecialize f) = f !== Core.Compiler.widenconst;
    
    julia> @test_opt function_filter=function_filter f(args...)
    ...

  • skip_unoptimized_throw_blocks::Bool = true:
    By default, Julia's native compilation pipeline intentionally disables inference (and so succeeding optimizations too) on "throw blocks", which are code blocks that will eventually lead to throw calls, in order to ease the compilation latency problem, a.k.a. "first-time-to-plot". Accordingly, the optimization analyzer also ignores any performance pitfalls detected within those blocks since we usually don't mind if code involved with error handling isn't optimized. If skip_unoptimized_throw_blocks is set to false, it doesn't ignore them and will report type instabilities detected within "throw blocks".

    See also https://github.com/JuliaLang/julia/pull/35982.

    # by default, unoptimized "throw blocks" are not analyzed
    julia> @test_opt sin(10)
    Test Passed
      Expression: #= none:1 =# JET.@test_opt sin(10)
    
    # we can turn on the analysis on unoptimized "throw blocks" with `skip_unoptimized_throw_blocks=false`
    julia> @test_opt skip_unoptimized_throw_blocks=false sin(10)
    JET-test failed at none:1
      Expression: #= REPL[6]:1 =# JET.@test_call analyzer = JET.OptAnalyzer skip_unoptimized_throw_blocks = false sin(10)
      ═════ 1 possible error found ═════
      ┌ @ math.jl:1221 Base.Math.sin(xf)
      │┌ @ special/trig.jl:39 Base.Math.sin_domain_error(x)
      ││┌ @ special/trig.jl:28 Base.Math.DomainError(x, "sin(x) is only defined for finite x.")
      │││ runtime dispatch detected: Base.Math.DomainError(x::Float64, "sin(x) is only defined for finite x.")::Any
      ││└──────────────────────
    
    ERROR: There was an error during testing
    
    # we can also turns off the heuristic itself
    julia> @test_opt unoptimize_throw_blocks=false skip_unoptimized_throw_blocks=false sin(10)
    Test Passed
      Expression: #= REPL[7]:1 =# JET.@test_call analyzer = JET.OptAnalyzer unoptimize_throw_blocks = false skip_unoptimized_throw_blocks = false sin(10)

JET.PrintConfigType

Configurations for report printing. The configurations below will be active whenever showing JET's analysis result within REPL.


  • fullpath::Bool = false
    Controls whether or not expand a file path to full path when printing analyzed call stack. Note that paths of Julia's Base files will also be expanded when set to true.

  • print_toplevel_success::Bool = false
    If true, prints a message when there is no toplevel errors found.

  • print_inference_success::Bool = true
    If true, print a message when there is no errors found in abstract interpretation based analysis pass.

JET.ReportPassType
abstract type ReportPass end

An interface type that represents AbstractAnalyzer's report pass. analyzer::AbstractAnalyzer injects report passes using the (::ReportPass)(::Type{InferenceErrorReport}, ::AbstractAnalyzer, state, ...) interface, which provides a flexible and efficient layer to configure the analysis done by AbstractAnalyzer.


ReportPass(analyzer::AbstractAnalyzer) -> ReportPass

If NewAnalyzer implements the AbstractAnalyzer interface, NewAnalyzer should implement this ReportPass(analyzer::NewAnalyzer) -> ReportPass interface.

ReportPass allows NewAnalyzer to provide a very flexible configuration layer for NewAnalyzer's analysis; an user can define their own ReportPass to control how NewAnalyzer collects report errors while still using the analysis routine implemented by NewAnalyzer.

Example

For example, JETAnalyzer accepts a custom ReportPass passed as part of the general configurations (see the documentation of AbstractAnalyzer for an example implementation). And we can setup a custom report pass IgnoreAllExceptGlobalUndefVar, that ignores all the reports that are otherwise collected by JETAnalyzer except UndefVarErrorReport:

# custom report pass that ignores all the reports except `UndefVarErrorReport`
struct IgnoreAllExceptGlobalUndefVar <: ReportPass end

# ignores all the reports analyzed by `JETAnalyzer`
(::IgnoreAllExceptGlobalUndefVar)(::Type{<:InferenceErrorReport}, @nospecialize(_...)) = return

# forward to `BasicPass` to collect `UndefVarErrorReport`
function (::IgnoreAllExceptGlobalUndefVar)(::Type{UndefVarErrorReport}, @nospecialize(args...))
    BasicPass()(UndefVarErrorReport, args...)
end

no_method_error()    = 1 + "1"
undef_global_error() = undefvar
report_call(; report_pass=IgnoreAllExceptGlobalUndefVar()) do
    if rand(Bool)
        return no_method_error()    # "no matching method found" error report won't be reported here
    else
        return undef_global_error() # "`undefvar` is not defined" error report will be reported
    end
end
JET.SeriousExceptionReportType
SeriousExceptionReport <: InferenceErrorReport

Represents a "serious" error that is manually thrown by a throw call. This is reported regardless of whether it's caught by control flow or not, as opposed to UncaughtExceptionReport.

JET.SignatureType
Signature

Represents an expression signature. print_signature implements a frontend functionality to show this type.

JET.ToplevelConfigType

Configurations for top-level analysis. These configurations will be active for all the top-level entries explained in the top-level analysis entry points section.


  • context::Module = Main
    The module context in which the top-level execution will be simulated.

    This configuration can be useful when you just want to analyze a submodule, without starting entire analysis from the root module. For example, we can analyze Base.Math like below:

    julia> report_file(JET.fullbasepath("math.jl");
                       context = Base,                  # `Base.Math`'s root module
                       analyze_from_definitions = true, # there're only definitions in `Base`
                       )

    Note that this module context will be virtualized by default so that JET can repeat analysis in the same session without having "invalid redefinition of constant ..." error etc. In other word, JET virtualize the module context of context and make sure the original module context isn't polluted by JET.


  • target_defined_modules::Bool = false
    If true, automatically set the target_modules configuration so that JET filters out errors that are reported within modules that JET doesn't analyze directly.

  • analyze_from_definitions::Bool = false
    If true, JET will start analysis using signatures of top-level definitions (e.g. method signatures), after the top-level interpretation has been done (unless no serious top-level error has happened, like errors involved within a macro expansion).

    This can be handy when you want to analyze a package, which usually contains only definitions but not their usages (i.e. top-level callsites). With this option, JET can enter analysis just with method or type definitions, and we don't need to pass a file that uses the target package.

    Warning

    This feature is very experimental at this point, and you may face lots of false positive errors, especially when trying to analyze a big package with lots of dependencies. If a file that contains top-level callsites (e.g. test/runtests.jl) is available, JET analysis using the file is generally preferred, since analysis entered from concrete call sites will produce more accurate results than analysis entered from (maybe not concrete-typed) method signatures.

    Also see: report_file, watch_file


  • concretization_patterns::Vector{Any} = Any[]
    Specifies a customized top-level code concretization strategy.

    When analyzing a top-level code, JET first splits the entire code into appropriate units of code (i.e. "code blocks"), and then iterate a virtual top-level code execution process on each code block in order to simulate Julia's top-level code execution. In the virtual code execution, JET will selectively interpret "top-level definitions" (like a function definition), while it tries to avoid executing any other parts of code including function calls that typically do a main computational task, leaving them to be analyzed by the succeeding abstract interpretation based analysis.

    However, currently, JET doesn't track "inter-block" level code dependencies, and therefore the selective interpretation of top-level definitions may fail when it needs to use global bindings defined in the other code blocks that have not been selected and actually interpreted (i.e. "concretized") but left for abstract interpretation (i.e. "abstracted").

    For example, the issue would happen if the expansion of a macro uses a global variable, e.g.:

    test/fixtures/concretization_patterns.jl

    # JET doesn't conretize this by default, but just analyzes its type
    const GLOBAL_CODE_STORE = Dict()
    
    macro with_code_record(a)
        GLOBAL_CODE_STORE[__source__] = a # record the code location in the global store
        esc(a)
    end
    
    # here JET will try to actually expand `@with_code_record`,
    # but since `GLOBAL_CODE_STORE` didn't get concretized (i.e. instantiated), JET analysis will fail at this point
    @with_code_record foo(a) = identity(a)
    
    foo(10) # top-level callsite, abstracted away
    

    To circumvent this issue, JET offers this concretization_patterns::Vector{<:Any} configuration, which allows us to customize JET's top-level code concretization strategy. concretization_patterns specifies the patterns of code that should be concretized. To put in other word, when JET sees a code that matches any of code patterns specified by this configuration, JET will try to interpret and concretize the code, regardless of whether or not JET's default code selection logic decides to concretize it.

    JET uses MacroTools.jl's expression pattern match, and we can specify whatever code pattern expected by MacroTools.@capture macro. For example, in order to solve the issue explained above, we can have:

    concretization_patterns = [:(const GLOBAL_CODE_STORE = Dict())]

    Then GLOBAL_CODE_STORE will just be concretized and so any top-level error won't happen at the macro expansion.

    Since configuring concretization_patterns properly can be tricky, JET offers a logging system that allows us to debug 's top-level code concretization plan. With the toplevel_logger configuration with specifying the logging level to be above than 1 ("debug") level, we can see:

    • which code is matched with concretization_patterns and forcibly concretized
    • which code is selected to be concretized by JET's default code selection logic: where t-annotated statements are concretized while f-annotated statements are abstracted
    julia> report_file("test/fixtures/concretization_patterns.jl";
                       concretization_patterns = [:(const GLOBAL_CODE_STORE = Dict())],
                       toplevel_logger = IOContext(stdout, :JET_LOGGER_LEVEL => 1))
    [toplevel-debug] virtualized the context of Main (took 0.003 sec)
    [toplevel-debug] entered into test/fixtures/concretization_patterns.jl
    [toplevel-debug] concretization pattern `const GLOBAL_CODE_STORE = Dict()` matched `const GLOBAL_CODE_STORE = Dict()` at test/fixtures/concretization_patterns.jl:2
    [toplevel-debug] concretization plan at test/fixtures/concretization_patterns.jl:4:
    1 f 1 ─      $(Expr(:thunk, CodeInfo(
        @ none within `top-level scope`
    1 ─     return $(Expr(:method, Symbol("@with_code_record")))
    )))
    2 t │        $(Expr(:method, Symbol("@with_code_record")))
    3 t │   %3 = Core.Typeof(var"@with_code_record")
    4 t │   %4 = Core.svec(%3, Core.LineNumberNode, Core.Module, Core.Any)
    5 t │   %5 = Core.svec()
    6 t │   %6 = Core.svec(%4, %5, $(QuoteNode(:(#= test/fixtures/concretization_patterns.jl:4 =#))))
    7 t │        $(Expr(:method, Symbol("@with_code_record"), :(%6), CodeInfo(
        @ test/fixtures/concretization_patterns.jl:5 within `none`
    1 ─      $(Expr(:meta, :nospecialize, :(a)))
    │        Base.setindex!(GLOBAL_CODE_STORE, a, __source__)
    │   @ test/fixtures/concretization_patterns.jl:6 within `none`
    │   %3 = esc(a)
    └──      return %3
    )))
    8 f └──      return var"@with_code_record"
    [toplevel-debug] concretization plan at test/fixtures/concretization_patterns.jl:11:
    1 f 1 ─      $(Expr(:thunk, CodeInfo(
        @ none within `top-level scope`
    1 ─     return $(Expr(:method, :foo))
    )))
    2 t │        $(Expr(:method, :foo))
    3 t │   %3 = Core.Typeof(foo)
    4 t │   %4 = Core.svec(%3, Core.Any)
    5 t │   %5 = Core.svec()
    6 t │   %6 = Core.svec(%4, %5, $(QuoteNode(:(#= test/fixtures/concretization_patterns.jl:11 =#))))
    7 t │        $(Expr(:method, :foo, :(%6), CodeInfo(
        @ test/fixtures/concretization_patterns.jl:11 within `none`
    1 ─ %1 = identity(a)
    └──      return %1
    )))
    8 f └──      return foo
    [toplevel-debug] concretization plan at test/fixtures/concretization_patterns.jl:13:
    1 f 1 ─ %1 = foo(10)
    2 f └──      return %1
    [toplevel-debug]  exited from test/fixtures/concretization_patterns.jl (took 0.032 sec)

    Also see: the toplevel_logger section below, virtual_process.

    Note

    report_package automatically sets this configuration as

    concretization_patterns = [:(x_)]

    meaning that it will concretize all top-level code included in a package being analyzed.


  • toplevel_logger::Union{Nothing,IO} = nothing
    If IO object is given, it will track JET's toplevel analysis. Logging level can be specified with :JET_LOGGER_LEVEL IO property. Currently supported logging levels are either of 0 ("info" level, default), 1 ("debug" level).

    Examples:

    • logs into stdout
    julia> report_file(filename; toplevel_logger = stdout)
    • logs into io::IOBuffer with "debug" logger level
    julia> report_file(filename; toplevel_logger = IOContext(io, :JET_LOGGER_LEVEL => 1));

  • virtualize::Bool = true
    When true, JET will virtualize the given root module context.

    This configuration is supposed to be used only for testing or debugging. See virtualize_module_context for the internal.


JET.ToplevelErrorReportType
ToplevelErrorReport

An interface type of error reports that JET collects while top-level concrete interpration. All ToplevelErrorReport should have the following fields:

  • file::String: the path to the file containing the interpretation context
  • line::Int: the line number in the file containing the interpretation context

See also: virtual_process, ConcreteInterpreter

JET.UncaughtExceptionReportType
UncaughtExceptionReport <: InferenceErrorReport

Represents general throw calls traced during inference. This is reported only when it's not caught by control flow.

JET.VirtualFrameType
VirtualFrame

Stack information representing virtual execution context:

  • file::Symbol: the path to the file containing the virtual execution context
  • line::Int: the line number in the file containing the virtual execution context
  • sig::Signature: a signature of this frame
  • linfo::MethodInstance: The MethodInstance containing the execution context

This type is very similar to Base.StackTraces.StackFrame, but its execution context is collected during abstract interpration, not collected from actual execution.

JET.VirtualProcessResultType
res::VirtualProcessResult
  • res.included_files::Set{String}: files that have been analyzed
  • res.defined_modules::Set{Module}: module contexts created while this top-level analysis
  • res.toplevel_error_reports::Vector{ToplevelErrorReport}: toplevel errors found during the text parsing or partial (actual) interpretation; these reports are "critical" and should have precedence over inference_error_reports
  • res.inference_error_reports::Vector{InferenceErrorReport}: possible error reports found by AbstractAnalyzer
  • res.toplevel_signatures: signatures of methods defined within the analyzed files
  • res.actual2virtual::Pair{Module, Module}: keeps actual and virtual module
JET.VirtualStackTraceType
VirtualStackTrace

Represents a virtual stack trace in the form of a vector of VirtualFrame. The vector holds VirtualFrames in order of "from entry call site to error point", i.e. the first element is the VirtualFrame of the entry call site, and the last element is that contains the error.

JET.WatchConfigType

Configurations for "watch" mode. The configurations will only be active when used with watch_file.


  • revise_all::Bool = true
    Redirected to Revise.entr's all keyword argument. When set to true, JET will retrigger analysis as soon as code updates are detected in any module tracked by Revise. Currently when encountering import/using statements, JET won't perform analysis, but rather will just load the modules as usual execution (this also means Revise will track those modules). So if you're editing both files analyzed by JET and modules that are used within the files, this configuration should be enabled.

  • revise_modules = nothing
    Redirected to Revise.entr's modules positional argument. If a iterator of Module is given, JET will retrigger analysis whenever code in modules updates.

    Tip

    This configuration is useful when your're also editing files that are not tracked by Revise, e.g. editing functions defined in Base:

    # re-perform analysis when you make a change to `Base`
    julia> watch_file(yourfile; revise_modules = [Base])

Core.Compiler.add_call_backedges!Method
add_call_backedges!(analyzer::JETAnalyzer, ...)

An overload for abstract_call_gf_by_type(analyzer::JETAnalyzer, ...), which always add backedges (even if a new method can't refine the return type grew up to Any). This is because a new method definition always has a potential to change JETAnalyzer's analysis result.

Core.Compiler.bail_out_callMethod
bail_out_call(analyzer::JETAnalyzer, ...)

This overload makes call inference performed by JETAnalyzer not bail out even when inferred return type grows up to Any to collect as much error reports as possible. That potentially slows down inference performance, but it would stay to be practical given that the number of matching methods are limited beforehand.

Core.Compiler.bail_out_toplevel_callMethod
bail_out_toplevel_call(analyzer::AbstractAnalyzer, ...)

This overload allows JET to keep inference performed by AbstractAnalyzer going on non-concrete call sites in a toplevel frame created by virtual_process.

Core.Compiler.const_prop_entry_heuristicMethod
const_prop_entry_heuristic(analyzer::JETAnalyzer, result::MethodCallResult, sv::InferenceState)

This overload forces constant prop' even if an inference result can't be improved anymore with respect to the return type, e.g. when result.rt is already Const. Especially, this overload implements an heuristic to force constant prop' when any error points have been reported while the previous abstract method call without constant arguments. The reason we want much more aggressive constant propagation by that heuristic is that it's highly possible constant prop' can produce more accurate analysis result, by throwing away false positive error reports by cutting off the unreachable control flow or detecting must-reachable throw calls.

Core.Compiler.inlining_policyMethod
inlining_policy(analyzer::AbstractAnalyzer, @nospecialize(src), ...) -> source::Any

Implements inlining policy for AbstractAnalyzer. Since AbstractAnalyzer works on InferenceResult whose src field keeps AnalysisResult or CachedAnalysisResult, this implementation needs to forward their wrapped source to inlining_policy(::AbstractInterpreter, ::Any, ::UInt8).

JET.JETInferenceParamsFunction

Configurations for abstract interpretation performed by JET. These configurations will be active for all the entries.

You can configure any of the keyword parameters that Core.Compiler.InferenceParams or Core.Compiler.OptimizationParams can take, e.g. max_methods:

julia> methods(==, (Any,Nothing))
# 3 methods for generic function "==" from Base:
 [1] ==(::Missing, ::Any)
     @ missing.jl:75
 [2] ==(w::WeakRef, v)
     @ gcutils.jl:4
 [3] ==(x, y)
     @ Base.jl:127

julia> report_call((Any,)) do x
           # when we account for all the possible matching method candidates,
           # `(::Missing == ::Nothing)::Missing` leads to an `NonBooleanCondErrorReport`
           x == nothing ? :nothing : :some
       end
═════ 1 possible error found ═════
┌ @ none:4 goto %4 if not x == nothing
│ non-boolean `Missing` found in boolean context (1/2 union split): goto %4 if not (x::Any == nothing)::Union{Missing, Bool}
└──────────

julia> report_call((Any,); max_methods=1) do x
           # since we limit `max_methods=1`, JET gives up analysis on `(x::Any == nothing)`
           # and thus we won't get any error report
           x == nothing ? :nothing : :some
       end
No errors detected

See also Core.Compiler.InferenceParams and Core.Compiler.OptimizationParams.

Listed below are selections of those parameters that can have a potent influence on JET analysis.


  • ipo_constant_propagation::Bool = true
    Enables constant propagation in abstract interpretation. It is highly recommended that you keep this configuration true to get reasonable analysis result, because constant propagation can cut off lots of false positive errorenous code paths and thus produce more accurate and useful analysis results.

  • aggressive_constant_propagation::Bool = true
    If true, JET will try to do constant propagation more "aggressively". It can lead to more accurate analysis as explained above, but also it may incur a performance cost. JET by default enables this configuration to get more accurate analysis result.

  • unoptimize_throw_blocks::Bool = false
    Turn this on to skip analysis on code blocks that will eventually lead to a throw call. This configuration improves the analysis performance, but it's better to be turned off to get a "proper" analysis result, just because there may be other errors even in those "throw blocks".

JET.aggregation_policyMethod
aggregation_policy(analyzer::AbstractAnalyzer)

Defines how analyzer aggregates InferenceErrorReports. Defaults to default_aggregation_policy.


default_aggregation_policy(report::InferenceErrorReport) -> DefaultReportIdentity

Returns the default identity of report::InferenceErrorReport, where DefaultReportIdentity aggregates reports based on "error location" of each report. DefaultReportIdentity aggregates InferenceErrorReports aggressively in a sense that it ignores the identity of error point's MethodInstance, under the assumption that errors are identical as far as they're collected at the same file and line.

JET.analyze_and_report_call!Method
analyze_and_report_call!(analyzer::AbstractAnalyzer, f, [types]; jetconfigs...) -> JETCallResult
analyze_and_report_call!(analyzer::AbstractAnalyzer, tt::Type{<:Tuple}; jetconfigs...) -> JETCallResult
analyze_and_report_call!(analyzer::AbstractAnalyzer, mi::MethodInstance; jetconfigs...) -> JETCallResult

A generic entry point to analyze a function call with AbstractAnalyzer. Finally returns the analysis result as JETCallResult. Note that this is intended to be used by developers of AbstractAnalyzer only. General users should use high-level entry points like report_call and report_opt.

JET.analyze_and_report_file!Function
analyze_and_report_file!(analyzer::AbstractAnalyzer, filename::AbstractString; jetconfigs...) -> JETToplevelResult

A generic entry point to analyze a file with AbstractAnalyzer. Finally returns the analysis result as JETToplevelResult. Note that this is intended to be used by developers of AbstractAnalyzer only. General users should use high-level entry points like report_file.

JET.analyze_and_report_package!Function
analyze_and_report_package!(analyzer::AbstractAnalyzer,
                            package::Union{AbstractString,Module,Nothing} = nothing;
                            jetconfigs...) -> JETToplevelResult

A generic entry point to analyze a package with AbstractAnalyzer. Finally returns the analysis result as JETToplevelResult. Note that this is intended to be used by developers of AbstractAnalyzer only. General users should use high-level entry points like report_package.

JET.analyze_and_report_text!Function
analyze_and_report_text!(analyzer::AbstractAnalyzer, text::AbstractString,
                         filename::AbstractString = "top-level";
                         jetconfigs...) -> JETToplevelResult

A generic entry point to analyze a top-level code with AbstractAnalyzer. Finally returns the analysis result as JETToplevelResult. Note that this is intended to be used by developers of AbstractAnalyzer only. General users should use high-level entry points like report_text.

JET.analyze_task_parallel_code!Method
analyze_task_parallel_code!(analyzer::AbstractAnalyzer, arginfo::ArgInfo, sv::InferenceState)

Adds special cased analysis pass for task parallelism. In Julia's task parallelism implementation, parallel code is represented as closure and it's wrapped in a Task object. Core.Compiler.NativeInterpreter doesn't infer nor optimize the bodies of those closures when compiling code that creates parallel tasks, but JET will try to run additional analysis pass by recurring into the closures.

See also: https://github.com/aviatesk/JET.jl/issues/114

Note

JET won't do anything other than doing JET analysis, e.g. won't annotate return type of wrapped code block in order to not confuse the original AbstractInterpreter routine track https://github.com/JuliaLang/julia/pull/39773 for the changes in native abstract interpretation routine.

JET.call_test_exMethod
call_test_ex(funcname::Symbol, testname::Symbol, ex0, __module__, __source__)

An internal utility function to implement a @test_call-like macro. See the implementation of @test_call.

JET.configured_reportsMethod

Configurations for JET's analysis results. These configurations are always active.


  • target_modules = nothing
    A configuration to filter out reports by specifying module contexts where problems should be reported.

    By default (target_modules = nothing), JET reports all detected problems. If specified, a problem is reported if its module context matches any of target_modules settings and hidden otherwise. target_modules should be an iterator of whose element is either of the data types below that match report::InferenceErrorReport's context module as follows:

    • m::Module or JET.LastFrameModule(m::Module): matches if the module context of report's innermost stack frame is m
    • JET.AnyFrameModule(m::Module): matches if module context of any of report's stack frame is m
    • user-type T: matches according to user-definition overload match_module(::T, report::InferenceErrorReport)

  • ignored_modules = nothing
    A configuration to filter out reports by specifying module contexts where problems should be ignored.

    By default (ignored_modules = nothing), JET reports all detected problems. If specified, a problem is hidden if its module context matches any of ignored_modules settings and reported otherwise. ignored_modules should be an iterator of whose element is either of the data types below that match report::InferenceErrorReport's context module as follows:

    • m::Module or JET.LastFrameModule(m::Module): matches if the module context of report's innermost stack frame is m
    • JET.AnyFrameModule(m::Module): matches if module context of any of report's stack frame is m
    • user-type T: matches according to user-definition overload match_module(::T, report::InferenceErrorReport)

  • report_config = nothing
    Additional configuration layer to filter out reports with user-specified strategies. By default (report_config = nothing), JET will use the module context based configurations elaborated above and below. If user-type T is given, then JET will report problems based on the logic according to an user-overload configured_reports(::T, reports::Vector{InferenceErrorReport}), and the target_modules and ignored_modules configurations are not really active.

Examples

julia> function foo(a)
           r1 = sum(a)       # => Base: MethodError(+(::Char, ::Char)), MethodError(zero(::Type{Char}))
           r2 = undefsum(a)  # => @__MODULE__: UndefVarError(:undefsum)
           return r1, r2
       end;

# by default, JET will print all the collected reports:
julia> @report_call foo("julia")
═════ 3 possible errors found ═════
┌ foo(a::String) @ Main ./REPL[14]:2
│┌ sum(a::String) @ Base ./reduce.jl:564
││┌ sum(a::String; kw::@Kwargs{}) @ Base ./reduce.jl:564
│││┌ sum(f::typeof(identity), a::String) @ Base ./reduce.jl:535
││││┌ sum(f::typeof(identity), a::String; kw::@Kwargs{}) @ Base ./reduce.jl:535
│││││┌ 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:383
││││││││││││┌ reduce_empty_iter(op::Base.BottomRF{typeof(Base.add_sum)}, itr::String, ::Base.HasEltype) @ Base ./reduce.jl:384
│││││││││││││┌ reduce_empty(op::Base.BottomRF{typeof(Base.add_sum)}, ::Type{Char}) @ Base ./reduce.jl:360
││││││││││││││┌ reduce_empty(::typeof(Base.add_sum), ::Type{Char}) @ Base ./reduce.jl:352
│││││││││││││││┌ reduce_empty(::typeof(+), ::Type{Char}) @ Base ./reduce.jl:343
││││││││││││││││ no matching method found `zero(::Type{Char})`: zero(T::Type{Char})
│││││││││││││││└────────────────────
┌ foo(a::String) @ Main ./REPL[14]:3
│ `Main.undefsum` is not defined: r2 = undefsum(a::String)
└────────────────────

# with `target_modules=(@__MODULE__,)`, JET will only report the problems detected within the `@__MODULE__` module:
julia> @report_call target_modules=(@__MODULE__,) foo("julia")
═════ 1 possible error found ═════
┌ foo(a::String) @ Main ./REPL[14]:3
│ `Main.undefsum` is not defined: r2 = undefsum(a::String)
└────────────────────

# with `ignored_modules=(Base,)`, JET will ignore the errors detected within the `Base` module:
julia> @report_call ignored_modules=(Base,) foo("julia")
═════ 1 possible error found ═════
┌ foo(a::String) @ Main ./REPL[14]:3
│ `Main.undefsum` is not defined: r2 = undefsum(a::String)
└────────────────────

JET.copy_reportMethod
copy_report(report::Report) where Report<:InferenceErrorReport -> new::Report

Returns new new::Report, that should be identical to the original report::Report, except that new.vst is copied from report.vst so that the further modification on report.vst that may happen in later abstract interpretation doesn't affect new.vst.

JET.func_testMethod
func_test(func, testname::Symbol, args...; jetconfigs...)

An internal utility function to implement a test_call-like function. See the implementation of test_call.

JET.islineageMethod
islineage(parent::MethodInstance, current::MethodInstance) ->
    (report::InferenceErrorReport) -> Bool

Returns a function that checks if a given InferenceErrorReport

  • is generated from current, and
  • is "lineage" of parent (i.e. entered from it).

This function is supposed to be used when additional analysis with extended lattice information happens in order to filter out reports collected from current by analysis without using that extended information. When a report should be filtered out, the first virtual stack frame represents parent and the second does current.

Example:

entry
└─ linfo1 (report1: linfo1->linfo2)
   ├─ linfo2 (report1: linfo2)
   ├─ linfo3 (report2: linfo3->linfo2)
   │  └─ linfo2 (report2: linfo2)
   └─ linfo3′ (~~report2: linfo3->linfo2~~)

In the example analysis above, report2 should be filtered out on re-entering into linfo3′ (i.e. when we're analyzing linfo3 with constant arguments), nevertheless report1 shouldn't because it is not detected within linfo3 but within linfo1 (so it's not a "lineage of linfo3"):

  • islineage(linfo1, linfo3)(report2) === true
  • islineage(linfo1, linfo3)(report1) === false
JET.parse_config_fileMethod

JET.jl offers .prettierrc style configuration file support. This means you can use .JET.toml configuration file to specify any of configurations explained above and share that with others.

When report_file or watch_file is called, it will look for .JET.toml in the directory of the given file, and search up the file tree until a JET configuration file is (or isn't) found. When found, the configurations specified in the file will be applied.

A configuration file can specify configurations like:

aggressive_constant_propagation = false # turn off aggressive constant propagation
... # other configurations

Note that the following configurations should be string(s) of valid Julia code:

  • context: string of Julia code, which can be parsed and evaluated into Module
  • concretization_patterns: vector of string of Julia code, which can be parsed into a Julia expression pattern expected by MacroTools.@capture macro.
  • toplevel_logger: string of Julia code, which can be parsed and evaluated into Union{IO,Nothing}

E.g. the configurations below are equivalent:

  • configurations via keyword arguments
    report_file(somefile;
                concretization_patterns = [:(const GLOBAL_CODE_STORE = x_)],
                toplevel_logger = IOContext(open("toplevel.txt", "w"), :JET_LOGGER_LEVEL => 1))
  • configurations via a configuration file
    # supposed to concretize `const GLOBAL_CODE_STORE = Dict()` in test/fixtures/concretization_patterns.jl
    concretization_patterns = ["const GLOBAL_CODE_STORE = Dict()"]
    
    # logs toplevel analysis into toplevel.txt with debug logging level
    toplevel_logger = """IOContext(open("toplevel.txt", "w"), :JET_LOGGER_LEVEL => 1)"""
    
Note

Configurations specified as keyword arguments have precedence over those specified via a configuration file.

JET.partially_interpret!Method
partially_interpret!(interp::ConcreteInterpreter, mod::Module, src::CodeInfo)

Partially interprets statements in src using JuliaInterpreter.jl:

  • concretizes "toplevel definitions", i.e. :method, :struct_type, :abstract_type and :primitive_type expressions and their dependencies
  • concretizes user-specified toplevel code (see ToplevelConfig)
  • directly evaluates module usage expressions and report error of invalid module usages (TODO: enter into the loaded module and keep JET analysis)
  • special-cases include calls so that top-level analysis recursively enters the included file
JET.print_report_messageMethod
print_report_message(io::IO, report::Report) where Report<:InferenceErrorReport

Prints to io and describes why report is reported.

JET.print_signatureMethod
print_signature(::Report) where Report<:InferenceErrorReport -> Bool

Configures whether or not to print the report signature when printing Report (defaults to true).

JET.report_callMethod
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.

JET.report_colorMethod
report_color(::Report) where Report<:InferenceErrorReport -> Symbol

Configures the color for Report (defaults to :red).

JET.report_fileMethod
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.

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

Analyzes a function call with the given type signature to detect optimization failures and unresolved method dispatches.

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

See the documentation of the optimization analysis for more details.

JET.report_packageMethod
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.

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

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

JET.test_callMethod
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.

JET.test_optMethod
test_opt(f, [types]; broken::Bool = false, skip::Bool = false, jetconfigs...)
test_opt(tt::Type{<:Tuple}; broken::Bool = false, skip::Bool = false, jetconfigs...)

Runs report_opt on a function call with the given type signature and tests that it is free from optimization failures and unresolved method dispatches that report_opt can detect. Except that it takes a type signature rather than a call expression, this function works in the same way as @test_opt.

JET.test_packageMethod
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
JET.valid_configurationsMethod
valid_configurations(analyzer::AbstractAnalyzer) -> names or nothing

Returns a set of names that are valid as a configuration for analyzer. names should be an iterator of Symbol. No validations are performed if nothing is returned.

JET.virtual_processMethod
virtual_process(s::AbstractString,
                filename::AbstractString,
                analyzer::AbstractAnalyzer,
                config::ToplevelConfig) -> res::VirtualProcessResult

Simulates Julia's toplevel execution and collects error points, and finally returns res::VirtualProcessResult

  • res.included_files::Set{String}: files that have been analyzed
  • res.defined_modules::Set{Module}: module contexts created while this top-level analysis
  • res.toplevel_error_reports::Vector{ToplevelErrorReport}: toplevel errors found during the text parsing or partial (actual) interpretation; these reports are "critical" and should have precedence over inference_error_reports
  • res.inference_error_reports::Vector{InferenceErrorReport}: possible error reports found by AbstractAnalyzer
  • res.toplevel_signatures: signatures of methods defined within the analyzed files
  • res.actual2virtual::Pair{Module, Module}: keeps actual and virtual module

This function first parses s::AbstractString into toplevelex::Expr and then iterate the following steps on each code block (blk) of toplevelex:

  1. if blk is a :module expression, recursively enters analysis into an newly defined virtual module
  2. lowers blk into :thunk expression lwr (macros are also expanded in this step)
  3. if the context module is virtualized, replaces self-references of the original context module with virtualized one: see fix_self_references
  4. ConcreteInterpreter partially interprets some statements in lwr that should not be abstracted away (e.g. a :method definition); see also partially_interpret!
  5. finally, AbstractAnalyzer analyzes the remaining statements by abstract interpretation
Warning

In order to process the toplevel code sequentially as Julia runtime does, virtual_process splits the entire code, and then iterate a simulation process on each code block. With this approach, we can't track the inter-code-block level dependencies, and so a partial interpretation of toplevle definitions will fail if it needs an access to global variables defined in other code blocks that are not interpreted but just abstracted. We can circumvent this issue using JET's concretization_patterns configuration, which allows us to customize JET's concretization strategy. See ToplevelConfig for more details.

JET.virtualize_module_contextMethod
virtualize_module_context(actual::Module)

HACK to return a module where the context of actual is virtualized.

The virtualization will be done by 2 steps below:

  1. loads the module context of actual into a sandbox module, and export the whole context from there
  2. then uses names exported from the sandbox

This way, JET's runtime simulation in the virtual module context will be able to define a name that is already defined in actual without causing "cannot assign a value to variable ... from module ..." error, etc. It allows JET to virtualize the context of already-existing module other than Main.

TODO

Currently this function relies on Base.names, and thus it can't restore the usinged names.

JET.watch_fileMethod
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.

JET.@jetreportMacro
@jetreport struct NewReport <: InferenceErrorReport
    ...
end

A utility macro to define InferenceErrorReport. It can be very tedious to manually satisfy the InferenceErrorReport interfaces. JET internally uses this @jetreport utility macro, which takes a struct definition of InferenceErrorReport without the required fields specified, and automatically defines the struct as well as constructor definitions. If the report NewReport <: InferenceErrorReport is defined using @jetreport, then NewReport just needs to implement the print_report_message interface.

For example, JETAnalyzer's MethodErrorReport is defined as follows:

@jetreport struct MethodErrorReport <: InferenceErrorReport
    @nospecialize t # ::Union{Type, Vector{Type}}
    union_split::Int
end
function print_report_message(io::IO, (; t, union_split)::MethodErrorReport)
    print(io, "no matching method found for ")
    if union_split == 0
        print_callsig(io, t)
    else
        ts = t::Vector{Any}
        nts = length(ts)
        for i = 1:nts
            print_callsig(io, ts[i])
            i == nts || print(io, ", ")
        end
        print(io, " (", nts, '/', union_split, " union split)")
    end
end

and constructed as like MethodErrorReport(sv::InferenceState, atype::Any, 0).

JET.@nospecsMacro
@nospecs def

Adds @nospecialize annotation to non-annotated arguments of def.

(Core.Compiler) julia> @macroexpand @nospecs function tfunc(𝕃::AbstractLattice, x, y::Bool, zs...)
                           x, ys
                       end
:(function tfunc($(Expr(:meta, :specialize, :(𝕃::AbstractLattice))), x, y::Bool, zs...)
      #= REPL[3]:1 =#
      $(Expr(:meta, :nospecialize, :x, :zs))
      #= REPL[3]:2 =#
      (x, ys)
  end)
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.
JET.@test_optMacro
@test_opt [jetconfigs...] [broken=false] [skip=false] f(args...)

Runs @report_opt jetconfigs... f(args...) and tests that the function call f(args...) is free from optimization failures and unresolved method dispatches that @report_opt can detect.

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

julia> function f(n)
            r = sincos(n)
            # `println` is full of runtime dispatches,
            # but we can ignore the corresponding reports from `Base`
            # with the `target_modules` configuration
            println(r)
            return r
       end;

julia> @test_opt target_modules=(@__MODULE__,) f(10)
Test Passed
  Expression: #= REPL[3]:1 =# JET.@test_call analyzer = JET.OptAnalyzer target_modules = (#= REPL[3]:1 =# @__MODULE__(),) f(10)

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

JET.@withmixedhashMacro
@withmixedhash (mutable) struct T
    fields ...
end

Defines struct T while automatically defining its Base.hash(::T, ::UInt) method which mixes hashes of all of T's fields (and also corresponding Base.:(==)(::T, ::T) method).

This macro is supposed to abstract the following kind of pattern:

https://github.com/aviatesk/julia/blob/999973df2850d6b2e0bd4bcf03ef90a14217b63c/base/pkgid.jl#L3-L25

struct PkgId
    uuid::Union{UUID,Nothing}
    name::String
end

==(a::PkgId, b::PkgId) = a.uuid == b.uuid && a.name == b.name

function hash(pkg::PkgId, h::UInt)
    h += 0xc9f248583a0ca36c % UInt
    h = hash(pkg.uuid, h)
    h = hash(pkg.name, h)
    return h
end

with @withmixedhash

@withmixedhash struct PkgId
    uuid::Union{UUID,Nothing}
    name::String
end