Configurations

JET analysis can be flexibly fine-tuned. Any entry point explained in Usages can accept any of the configuration parameter described below as keyword arguments (or optional parameters for macros). For example, if you want to analyze your_awesome_file.jl with turning on strict_condition_check configuration and also logs inference process into stdout, your can do:

report_file("your_awesome_file.jl";
            strict_condition_check = true,
            inference_logger = stdout)
Note

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

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 Analysis entry points.


  • 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 is useful when you want to analyze a package, which usually contains only definitions but not 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 entered from there will produce more accurate analysis results than using with this configuration.

    Also see: report_file, report_and_watch_file


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

    When analyzing a top-level code, JET first splits the entire code and then iterate a virtual top-level code execution process on each code block, in order to simulate Julia's sequential top-level code execution. However, with this approach, JET can't track the "inter-code-block" level dependencies, and so a partial interpretation of top-level definitions can fail if it needs an access to global variables defined in other code blocks that are not actually interpreted ("concretized") but just abstract-interpreted ("abstracted").

    For example, the issue happens when your macro accesses to a global variable during its expansion, e.g.:

    test/fixtures/concretization_patterns.jl

    # JET doesn't conretize this by default, but just analyze 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 fails at this point
    @with_code_record foo(a) = identity(a)
    
    foo(10) # top-level callsite, abstracted away
    

    To circumvent this issue, JET offers the 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. JET internally uses MacroTools.jl's expression pattern match, and we can specify any expression pattern that is expected by MacroTools.@capture macro. For example, in order to solve the issue explained above, we can have:

    concretization_patterns = [:(GLOBAL_CODE_STORE = x_)]

    Please note that we must use :(GLOBAL_CODE_STORE = x_) rather than :(const GLOBAL_CODE_STORE = x_). This is because currently the specified patterns will be matched against the lowered code representation, in which const x = y has been lowered to the sequence of 1.) the declaration const x, 2.) value computation %2 = Dict() and 3.) actual assignment part x = %2. Although this could be really tricky, we can effectively debug JET's top-level code concretization plan using JETLogger's toplevel_logger with the logging level above than 1 ("debug") level, where t-annotated statements will be concretize while f-annotated statements will be analyzed by abstract interpretation.

    julia> report_file("test/fixtures/concretization_patterns.jl";
                       concretization_patterns = [:(GLOBAL_CODE_STORE = x_)],
                       toplevel_logger = IOContext(stdout, :JET_LOGGER_LEVEL => 1))
    [toplevel-debug] entered into test/fixtures/concretization_patterns.jl
    [toplevel-debug] concretization plan:
    1 f 1 ─      const GLOBAL_CODE_STORE
    2 t │   %2 = Dict()
    3 t │        GLOBAL_CODE_STORE = %2
    4 f └──      return %2
    [toplevel-debug] concretization plan:
    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:
    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:
    1 f 1 ─ %1 = foo(10)
    2 f └──      return %1
    [toplevel-debug]  exited from test/fixtures/concretization_patterns.jl (took 0.018 sec)

    Also see: Logging Configurations, virtual_process.


source

Configurations for Abstract Interpretation

Warning

Currently JET doesn't invalidate the previous analysis results cached with the different analysis configurations (see https://github.com/aviatesk/JET.jl/issues/130 for an example). So you need to refresh Julia session to get updated analysis results with different inference configurations.

JET.JETAnalysisParamsType

Configurations for JET analysis. These configurations will be active for all the entries.


  • strict_condition_check::Bool = false
    Enables strict condition check. JET reports an error if a condition expression type is "non-boolean". In a case when the condition type is Union, JET will report if either of union split case is non-boolean type, but this can lead to lots of false positive error reports when the code is not well-typed, because Julia Base defines generic functions that are commonly used at a conditional context but also may return "non-boolean" values, e.g.:

    • !(::Function) -> Function
    • !(::Missing) -> Missing
    • ==(::Missing, ::Any) -> Missing
    • ==(::Any, ::Missing) -> Missing

    and thus loosely-typed conditional expression often becomes e.g. Union{Bool, Missing}, and consequently JET will report it as "non-boolean" type (NOTE: in Julia Missing is certainly not valid conditional type). If this configuration is set to false, JET enables an heuristic to avoid those false positive error reports and won't report an error if a condition expression type is Union and either of its union split case is Function or Missing.

    The effect of this configuration can be described with the following examples:

    • with strict_condition_check::Bool = false (default)

      julia> test_f() = Dict('a' => 1, :b => 2) # ::Dict{Any,Int}
      test_f (generic function with 1 method)
      
      julia> @report_call test_f()
      No errors !
      Dict{Any, Int64}
    • with strict_condition_check::Bool = true

      julia> test_f() = Dict('a' => 1, :b => 2) # ::Dict{Any,Int}
      test_f (generic function with 1 method)
      
      julia> @report_call strict_condition_check = true test_f()
      ═════ 1 possible error found ═════
      ┌ @ REPL[2]:1 Main.Dict(Main.=>('a', 1), Main.=>(:b, 2))
      │┌ @ dict.jl:125 Base.Dict(ps)
      ││┌ @ dict.jl:129 Base.dict_with_eltype(#308, kv, Base.eltype(kv))
      │││┌ @ abstractdict.jl:539 Base.grow_to!(Base.dict_with_eltype(DT_apply, _5), kv)
      ││││┌ @ dict.jl:145 Base.grow_to!(dest2, itr, st)
      │││││┌ @ dict.jl:159 Base.setindex!(new, v, k)
      ││││││┌ @ dict.jl:383 Base.ht_keyindex2!(h, key)
      │││││││┌ @ dict.jl:328 goto %35 if not Base.isequal(key, Base.getindex(keys, index))
      ││││││││ for 1 of union split cases, non-boolean (Missing) used in boolean context: goto %35 if not Base.isequal(key::Symbol, Base.getindex(keys::Vector{Any}, index::Int64)::Any)::Union{Missing, Bool}
      │││││││└───────────────
      Dict{Any, Int64}

  • ignore_native_remarks::Bool = true
    If true, JET won't construct nor cache reports of "native remarks", which may speed up analysis time. "Native remarks" are information that Julia's native compiler emits about how type inference routine goes, and those remarks are less interesting in term of "error checking", so JET ignores them by default.
source
JET.JETInferenceParamsFunction

Configurations for Julia's native type inference routine. These configurations will be active for all the entries.

You can specify all the keyword parameters of Core.Compiler.InferenceParams, e.g. max_methods::Int = 3, union_splitting::Int = 4. Listed here 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, because constant propagation can cut off lots of false positive errorenous code paths and thus lead to more accurate and useful analysis results.

  • aggressive_constant_propagation::Bool = true
    If true, JET will try to do constant propagation more "aggressively". As explained above, it can lead to more accurate analysis, but also lead to worse analysis performance at the cost of that.

  • unoptimize_throw_blocks::Bool = false
    Turn this on to skip analysis on code blocks that will eventually lead to a throw call. This configuration may improve the analysis performance, but it's better to be turned off for JET analysis, because there may be other errors even in those code blocks.
source
JET.PrintConfigType

JET configurations for error printing. If the entry renders the collected error points, the configurations below will be active.


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

  • annotate_types::Bool = false
    When set to true, annotates types when printing analyzed call stack. Here are examples:

    • with annotate_types = false (default):
      julia> @report_call sum("julia")
      ═════ 2 possible errors found ═════
      ┌ @ reduce.jl:530 Base.#sum#241(Base.pairs(Core.NamedTuple()), #self#, a)
      │┌ @ reduce.jl:530 Base.sum(Base.identity, a)
      ││┌ @ reduce.jl:503 Base.#sum#240(Base.pairs(Core.NamedTuple()), #self#, f, a)
      │││┌ @ reduce.jl:503 Base.mapreduce(f, Base.add_sum, a)
      ││││┌ @ reduce.jl:289 Base.#mapreduce#237(Base.pairs(Core.NamedTuple()), #self#, f, op, itr)
      │││││┌ @ reduce.jl:289 Base.mapfoldl(f, op, itr)
      ││││││┌ @ reduce.jl:162 Base.#mapfoldl#233(Base._InitialValue(), #self#, f, op, itr)
      │││││││┌ @ reduce.jl:162 Base.mapfoldl_impl(f, op, init, itr)
      ││││││││┌ @ reduce.jl:44 Base.foldl_impl(op′, nt, itr′)
      │││││││││┌ @ reduce.jl:48 Base._foldl_impl(op, nt, itr)
      ││││││││││┌ @ reduce.jl:62 op(v, Base.getindex(y, 1))
      │││││││││││┌ @ reduce.jl:81 Base.getproperty(op, :rf)(acc, x)
      ││││││││││││┌ @ reduce.jl:24 Base.+(x, y)
      │││││││││││││ no matching method found for call signature: Base.+(x::Char, y::Char)
      ││││││││││││└────────────────
      │││││││││┌ @ reduce.jl:49 Base.reduce_empty_iter(op, itr)
      ││││││││││┌ @ reduce.jl:356 Base.reduce_empty_iter(op, itr, Base.IteratorEltype(itr))
      │││││││││││┌ @ reduce.jl:357 Base.reduce_empty(op, Base.eltype(itr))
      ││││││││││││┌ @ reduce.jl:330 Base.reduce_empty(Base.getproperty(op, :rf), _)
      │││││││││││││┌ @ reduce.jl:322 Base.reduce_empty(Base.+, _)
      ││││││││││││││┌ @ reduce.jl:313 Base.zero(_)
      │││││││││││││││ no matching method found for call signature: Base.zero(_::Type{Char})
      ││││││││││││││└─────────────────
      Char
    • with annotate_types = true
      julia> @report_call annotate_types = true sum("julia")
      ═════ 2 possible errors found ═════
      ┌ @ reduce.jl:530 Base.#sum#241(Base.pairs(Core.NamedTuple()::NamedTuple{(), Tuple{}})::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, #self#::typeof(sum), a::String)
      │┌ @ reduce.jl:530 Base.sum(Base.identity, a::String)
      ││┌ @ reduce.jl:503 Base.#sum#240(Base.pairs(Core.NamedTuple()::NamedTuple{(), Tuple{}})::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, #self#::typeof(sum), f::typeof(identity), a::String)
      │││┌ @ reduce.jl:503 Base.mapreduce(f::typeof(identity), Base.add_sum, a::String)
      ││││┌ @ reduce.jl:289 Base.#mapreduce#237(Base.pairs(Core.NamedTuple()::NamedTuple{(), Tuple{}})::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, #self#::typeof(mapreduce), f::typeof(identity), op::typeof(Base.add_sum), itr::String)
      │││││┌ @ reduce.jl:289 Base.mapfoldl(f::typeof(identity), op::typeof(Base.add_sum), itr::String)
      ││││││┌ @ reduce.jl:162 Base.#mapfoldl#233(Base._InitialValue()::Base._InitialValue, #self#::typeof(mapfoldl), f::typeof(identity), op::typeof(Base.add_sum), itr::String)
      │││││││┌ @ reduce.jl:162 Base.mapfoldl_impl(f::typeof(identity), op::typeof(Base.add_sum), init::Base._InitialValue, itr::String)
      ││││││││┌ @ reduce.jl:44 Base.foldl_impl(op′::Base.BottomRF{typeof(Base.add_sum)}, nt::Base._InitialValue, itr′::String)
      │││││││││┌ @ reduce.jl:49 Base.reduce_empty_iter(op::Base.BottomRF{typeof(Base.add_sum)}, itr::String)
      ││││││││││┌ @ reduce.jl:356 Base.reduce_empty_iter(op::Base.BottomRF{typeof(Base.add_sum)}, itr::String, Base.IteratorEltype(itr::String)::Base.HasEltype)
      │││││││││││┌ @ reduce.jl:357 Base.reduce_empty(op::Base.BottomRF{typeof(Base.add_sum)}, Base.eltype(itr::String)::Type{Char})
      ││││││││││││┌ @ reduce.jl:330 Base.reduce_empty(Base.getproperty(op::Base.BottomRF{typeof(Base.add_sum)}, :rf::Symbol)::typeof(Base.add_sum), _::Type{Char})
      │││││││││││││┌ @ reduce.jl:322 Base.reduce_empty(Base.+, _::Type{Char})
      ││││││││││││││┌ @ reduce.jl:313 Base.zero(_::Type{Char})
      │││││││││││││││ no matching method found for call signature: Base.zero(_::Type{Char})
      ││││││││││││││└─────────────────
      │││││││││┌ @ reduce.jl:48 Base._foldl_impl(op::Base.BottomRF{typeof(Base.add_sum)}, nt::Base._InitialValue, itr::String)
      ││││││││││┌ @ reduce.jl:62 op::Base.BottomRF{typeof(Base.add_sum)}(v::Char, Base.getindex(y::Tuple{Char, Int64}, 1)::Char)
      │││││││││││┌ @ reduce.jl:81 Base.getproperty(op::Base.BottomRF{typeof(Base.add_sum)}, :rf::Symbol)::typeof(Base.add_sum)(acc::Char, x::Char)
      ││││││││││││┌ @ reduce.jl:24 Base.+(x::Char, y::Char)
      │││││││││││││ no matching method found for call signature: Base.+(x::Char, y::Char)
      ││││││││││││└────────────────
      Char
    Note

    JET always annotates types when printing the error point, e.g. in the example above, the error points below are always type-annotated regardless of this configuration:

    • no matching method found for call signature: Base.zero(_::Type{Char})
    • no matching method found for call signature: Base.+(x::Char, y::Char)

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

Watch Configurations

JET.WatchConfigType

Configurations for "watch" mode. The configurations will only be active when used with report_and_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-performe analysis when you make a change to `Base`
    julia> report_and_watch_file(yourfile; revise_modules = [Base])
source

Logging Configurations

JET.JETLoggerType

Logging configurations for JET analysis.


  • 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));

  • inference_logger::Union{Nothing,IO} = nothing
    If IO object is given, it will track JET's abstract interpretation routine. 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
      analyze_call(f, args...; inference_logger = stdout)
    • logs into io::IOBuffer with "debug" logger level
      julia> analyze_call(f, args...; inference_logger = IOContext(io, :JET_LOGGER_LEVEL => 1))

Tip

Of course you can specify both toplevel_logger and inference_logger at the same time like below:

julia> report_and_watch_file(filename;
                             toplevel_logger = IOContext(logger_io, :JET_LOGGER_LEVEL => 1),
                             inference_logger = inference_io)
source

Configuration File

JET.parse_configFunction

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 report_and_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 any of JET 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:

  • 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}
  • inference_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 = [:(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 = ["GLOBAL_CODE_STORE = x_"]
    
    # logs toplevel analysis into toplevel.txt with debug logging level
    toplevel_logger = """IOContext(open("toplevel.txt", "w"), :JET_LOGGER_LEVEL => 1)"""
    
source