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)
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_reports
— FunctionConfigurations 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 oftarget_modules
settings and hidden otherwise.target_modules
should be an iterator of whose element is either of the data types below that matchreport::InferenceErrorReport
's context module as follows:m::Module
orJET.LastFrameModule(m::Module)
: matches if the module context ofreport
's innermost stack frame ism
JET.AnyFrameModule(m::Module)
: matches if module context of any ofreport
's stack frame ism
- user-type
T
: matches according to user-definition overloadmatch_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 ofignored_modules
settings and reported otherwise.ignored_modules
should be an iterator of whose element is either of the data types below that matchreport::InferenceErrorReport
's context module as follows:m::Module
orJET.LastFrameModule(m::Module)
: matches if the module context ofreport
's innermost stack frame ism
JET.AnyFrameModule(m::Module)
: matches if module context of any ofreport
's stack frame ism
- user-type
T
: matches according to user-definition overloadmatch_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-typeT
is given, then JET will report problems based on the logic according to an user-overloadconfigured_reports(::T, reports::Vector{InferenceErrorReport})
, and thetarget_modules
andignored_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)
└────────────────────
Configurations for Top-level Analysis
JET.ToplevelConfig
— TypeConfigurations 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
Iftrue
, automatically set thetarget_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
Iftrue
, 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 asname::Symbol
, JET starts its analysis using the interpreted method signature whose name is equal toname
as the analysis entry point. For example, when analyzing a script that uses@main
to specify the entry point, it would be convenient to specifyanalyze_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 thetoplevel_logger
configuration with specifying the logging level to be above than1
("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 whilef
-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 asconcretization_patterns = [:(x_)]
meaning that it will concretize all top-level code included in a package being analyzed.
- which code is matched with
toplevel_logger::Union{Nothing,IO} = nothing
IfIO
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 of0
("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));
- logs into
virtualize::Bool = true
Whentrue
, 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.
Configurations for Abstract Interpretation
JET.JETInferenceParams
— FunctionConfigurations 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 configurationtrue
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
Iftrue
, 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 athrow
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".
Core.Compiler.InferenceParams
— Typeinf_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 thanmax_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 toCore._apply_iterate
.
inf_params.max_tuple_splat::Int = 32
When attempting to infer a call toCore._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
Iffalse
, 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
Iftrue
, 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
Iftrue
, skips inferring calls that are in a block that is known tothrow
. It may improve the compiler latency without sacrificing the runtime performance in common situations.
inf_params.assume_bindings_static::Bool = false
Iftrue
, 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 willthrow
). Defaults tofalse
since this assumption does not hold in Julia's semantics for native code execution.
Core.Compiler.OptimizationParams
— Typeopt_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 toopt_params.inline_cost_threshold
when making inlining decision.
opt_params.inline_error_path_cost::Int = 20
Specifies the penalty cost for an un-optimized dynamic call in a block that is known tothrow
. See also(inf_params::InferenceParams).unoptimize_throw_blocks
.
opt_params.max_tuple_splat::Int = 32
When attempting to inlineCore._apply_iterate
, abort the optimization if the tuple contains more than this many elements.
opt_params.compilesig_invokes::Bool = true
Iftrue
, gives the inliner license to change whichMethodInstance
to invoke when generating:invoke
expression based on the@nospecialize
annotation, in order to avoid over-specialization.
opt_params.assume_fatal_throw::Bool = false
Iftrue
, gives the optimizer license to assume that anythrow
is fatal and thus the state after athrow
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 tofalse
.
opt_params.preserve_local_sources::Bool = false
Iftrue
, the inliner is restricted from modifying locally-cached sources that are retained inCallInfo
objects and always makes their copies before inlining them into caller context. Defaults tofalse
.
Print Configurations
JET.PrintConfig
— TypeConfigurations for report printing. The configurations below will be active whenever show
ing 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'sBase
files will also be expanded when set totrue
.
print_toplevel_success::Bool = false
Iftrue
, prints a message when there is no toplevel errors found.
print_inference_success::Bool = true
Iftrue
, print a message when there is no errors found in abstract interpretation based analysis pass.
stacktrace_types_limit::Union{Nothing, Int} = nothing
Ifnothing
, limit type-depth printing of argument types in stack traces based on the display size. If a positiveInt
, limit type-depth printing to given depth. If a non-positiveInt
, do not limit type-depth printing.
Configurations for VSCode Integration
JET.VSCode.VSCodeConfig
— TypeConfigurations 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. Ifvscode_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. Whennothing
, the result will be only shown in the integrated views.
Watch Configurations
JET.WatchConfig
— TypeConfigurations for "watch" mode. The configurations will only be active when used with watch_file
.
revise_all::Bool = true
Redirected toRevise.entr
'sall
keyword argument. When set totrue
, JET will retrigger analysis as soon as code updates are detected in any module tracked by Revise. Currently when encounteringimport/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 toRevise.entr
'smodules
positional argument. If a iterator ofModule
is given, JET will retrigger analysis whenever code inmodules
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])
Configuration File
JET.parse_config_file
— FunctionJET.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 beparse
d andeval
uated intoModule
concretization_patterns
: vector of string of Julia code, which can beparse
d into a Julia expression pattern expected byMacroTools.@capture
macro.toplevel_logger
: string of Julia code, which can beparse
d andeval
uated intoUnion{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)"""
Configurations specified as keyword arguments have precedence over those specified via a configuration file.