General Configurations

JET can be fine-tuned very flexibly. Any entry point explained in JET's default error analysis and the optimization analysis can accept any of the configuration parameters described below as keyword arguments (or optional parameters for the interactive macros). For example, you can analyze the call of sum("julia") with the fullpath configuration enabled as like:

@report_call fullpath=true sum("julia")

or equivalently:

report_call(sum, (String,); fullpath=true)

Similarly you can analyze a top-level script path/to/file.jl with specifying target_defined_modules configuration as follows:

report_file("path/to/file.jl";
            target_defined_modules = true)
Note

Please ignore the names of documented objects appearing below, like "JET.configured_reports". They are just remnants of documentation internals, and you will never directly interact with them.

Configurations for Analysis Result

JET.configured_reportsFunction

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)
└────────────────────

source

Configurations for Top-level Analysis

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 virtualizes 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::Union{Bool,Symbol} = 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.

    When analyze_from_definitions is specified as name::Symbol, JET starts its analysis using the interpreted method signature whose name is equal to name as the analysis entry point. For example, when analyzing a script that uses @main to specify the entry point, it would be convenient to specify analyze_from_definitions = :main.

    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, report_package


  • 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.


source

Configurations for Abstract Interpretation

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".

source
Core.Compiler.InferenceParamsType
inf_params::InferenceParams

Parameters that control abstract interpretation-based type inference operation.


  • inf_params.max_methods::Int = 3
    Type inference gives up analysis on a call when there are more than max_methods matching methods. This trades off between compiler latency and generated code performance. Typically, considering many methods means spending lots of time obtaining poor type information, so this option should be kept low. Base.Experimental.@max_methods can have a more fine-grained control on this configuration with per-module or per-method annotation basis.

  • inf_params.max_union_splitting::Int = 4
    Specifies the maximum number of union-tuples to swap or expand before computing the set of matching methods or conditional types.

  • inf_params.max_apply_union_enum::Int = 8
    Specifies the maximum number of union-tuples to swap or expand when inferring a call to Core._apply_iterate.

  • inf_params.max_tuple_splat::Int = 32
    When attempting to infer a call to Core._apply_iterate, abort the analysis if the tuple contains more than this many elements.

  • inf_params.tuple_complexity_limit_depth::Int = 3
    Specifies the maximum depth of large tuple type that can appear as specialized method signature when inferring a recursive call graph.

  • inf_params.ipo_constant_propagation::Bool = true
    If false, disables analysis with extended lattice information, i.e. disables any of the concrete evaluation, semi-concrete interpretation and constant propagation entirely. Base.@constprop :none can have a more fine-grained control on this configuration with per-method annotation basis.

  • inf_params.aggressive_constant_propagation::Bool = false
    If true, forces constant propagation on any methods when any extended lattice information available. Base.@constprop :aggressive can have a more fine-grained control on this configuration with per-method annotation basis.

  • inf_params.unoptimize_throw_blocks::Bool = true
    If true, skips inferring calls that are in a block that is known to throw. It may improve the compiler latency without sacrificing the runtime performance in common situations.

  • inf_params.assume_bindings_static::Bool = false
    If true, assumes that no new bindings will be added, i.e. a non-existing binding at inference time can be assumed to always not exist at runtime (and thus e.g. any access to it will throw). Defaults to false since this assumption does not hold in Julia's semantics for native code execution.

source
Core.Compiler.OptimizationParamsType
opt_params::OptimizationParams

Parameters that control optimizer operation.


  • opt_params.inlining::Bool = inlining_enabled()
    Controls whether or not inlining is enabled.

  • opt_params.inline_cost_threshold::Int = 100
    Specifies the number of CPU cycles beyond which it's not worth inlining.

  • opt_params.inline_nonleaf_penalty::Int = 1000
    Specifies the penalty cost for a dynamic dispatch.

  • opt_params.inline_tupleret_bonus::Int = 250
    Specifies the extra inlining willingness for a method specialization with non-concrete tuple return types (in hopes of splitting it up). opt_params.inline_tupleret_bonus will be added to opt_params.inline_cost_threshold when making inlining decision.


  • opt_params.max_tuple_splat::Int = 32
    When attempting to inline Core._apply_iterate, abort the optimization if the tuple contains more than this many elements.

  • opt_params.compilesig_invokes::Bool = true
    If true, gives the inliner license to change which MethodInstance to invoke when generating :invoke expression based on the @nospecialize annotation, in order to avoid over-specialization.

  • opt_params.assume_fatal_throw::Bool = false
    If true, gives the optimizer license to assume that any throw is fatal and thus the state after a throw is not externally observable. In particular, this gives the optimizer license to move side effects (that are proven not observed within a particular code path) across a throwing call. Defaults to false.

  • opt_params.preserve_local_sources::Bool = false
    If true, the inliner is restricted from modifying locally-cached sources that are retained in CallInfo objects and always makes their copies before inlining them into caller context. Defaults to false.

source
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.

  • stacktrace_types_limit::Union{Nothing, Int} = nothing
    If nothing, limit type-depth printing of argument types in stack traces based on the display size. If a positive Int, limit type-depth printing to given depth. If a non-positive Int, do not limit type-depth printing.

source

Configurations for VSCode Integration

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.

source

Watch Configurations

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])

source

Configuration File

JET.parse_config_fileFunction

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.

source