JET.JET
— ModuleJET.jl
JET employs Julia's type inference system to detect potential bugs and type instabilities.
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.
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.
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)
═════ 1 possible error found ═════
┌ @ reduce.jl:198 Base.:(var"#foldl#291")(kw..., _3, op, itr)
│┌ @ reduce.jl:198 Core.kwcall(merge(Base.NamedTuple(), kw), mapfoldl, identity, op, itr)
││┌ @ reduce.jl:175 Base.:(var"#mapfoldl#290")(_8, _3, f, op, itr)
│││┌ @ reduce.jl:175 Base.mapfoldl_impl(f, op, init, itr)
││││┌ @ reduce.jl:44 Base.foldl_impl(op′, nt, itr′)
│││││┌ @ reduce.jl:48 v = Base._foldl_impl(op, nt, itr)
││││││┌ @ reduce.jl:62 op(%20, %37)
│││││││ runtime dispatch detected: op::Base.BottomRF{typeof(+)}(%20::Any, %37::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 ═════
┌ @ reduce.jl:198 Base.:(var"#foldl#291")(pairs(NamedTuple()), #self#, op, itr)
│┌ @ reduce.jl:198 mapfoldl(identity, op, itr)
││┌ @ reduce.jl:175 Base.:(var"#mapfoldl#290")(Base._InitialValue(), #self#, f, op, itr)
│││┌ @ reduce.jl:175 Base.mapfoldl_impl(f, op, init, itr)
││││┌ @ reduce.jl:44 Base.foldl_impl(op′, nt, itr′)
│││││┌ @ reduce.jl:48 v = Base._foldl_impl(op, nt, itr)
││││││┌ @ reduce.jl:62 v = op(v, y[1])
│││││││┌ @ reduce.jl:86 op.rf(acc, x)
││││││││ no matching method found `+(::Char, ::Char)`: (op::Base.BottomRF{typeof(+)}).rf::typeof(+)(acc::Char, x::Char)
│││││││└────────────────
│││││┌ @ reduce.jl:49 Base.reduce_empty_iter(op, itr)
││││││┌ @ reduce.jl:383 Base.reduce_empty_iter(op, itr, Base.IteratorEltype(itr))
│││││││┌ @ reduce.jl:384 Base.reduce_empty(op, eltype(itr))
││││││││┌ @ reduce.jl:360 Base.reduce_empty(op.rf, T)
│││││││││┌ @ reduce.jl:343 zero(T)
││││││││││ 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 AbstractTrees
julia> report_package(AbstractTrees)
[ some output elided ]
═════ 4 possible errors found ═════
┌ @ ~/.julia/packages/AbstractTrees/x9S7q/src/base.jl:260 AbstractTrees.collect(Core.apply_type(StableNode, T), ch)
│┌ @ array.jl:647 Base._collect(T, itr, Base.IteratorSize(itr))
││┌ @ array.jl:649 Base._array_for(T, isz, Base._similar_shape(itr, isz))
│││┌ @ array.jl:679 Base._similar_shape(itr, isz)
││││┌ @ array.jl:664 axes(itr)
│││││┌ @ abstractarray.jl:95 size(A)
││││││ no matching method found `size(::Base.HasLength)`: size(A::Base.HasLength)
│││││└───────────────────────
││││┌ @ array.jl:663 length(itr)
│││││ no matching method found `length(::Base.HasLength)`: length(itr::Base.HasLength)
││││└────────────────
┌ @ ~/.julia/packages/AbstractTrees/x9S7q/src/indexing.jl:137 AbstractTrees.idx.tree
│ `AbstractTrees.idx` is not defined
└───────────────────────────────────────────────────────────────────────
┌ @ ~/.julia/packages/AbstractTrees/x9S7q/src/indexing.jl:137 AbstractTrees.idx.index
│ `AbstractTrees.idx` is not defined
└───────────────────────────────────────────────────────────────────────
Limitations
JET explores the functions you call directly as well as their inferrable 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_TABLE
— ConstantJET_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.AbstractAnalyzer
— Typeabstract 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:
NewAnalyzer(; jetconfigs...) -> NewAnalyzer
:
Constructs new analyzer given JET configurations passed asjetconfigs
.
AnalyzerState(analyzer::NewAnalyzer) -> AnalyzerState
:
Returns theAnalyzerState
foranalyzer::NewAnalyzer
.
AbstractAnalyzer(analyzer::NewAnalyzer, state::AnalyzerState) -> NewAnalyzer
:
Constructs an newNewAnalyzer
instance in the middle of JET's top-level analysis or abstract interpretation, given the previousanalyzer::NewAnalyzer
andstate::AnalyzerState
.
ReportPass(analyzer::NewAnalyzer) -> ReportPass
:
ReturnsReportPass
used foranalyzer::NewAnalyzer
.
AnalysisCache(analyzer::NewAnalyzer) -> analysis_cache::AnalysisCache
:
Returns code cache used foranalyzer::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
function JETAnalyzer(;
report_pass::ReportPass = BasicPass(),
jetconfigs...)
state = AnalyzerState(; jetconfigs...)
analysis_cache = AnalysisCache() # TODO globalize this
return JETAnalyzer(state, analysis_cache, report_pass)
end
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.AbstractBuiltinErrorReport
— TypeAbstractBuiltinErrorReport
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.AbstractGlobal
— Typemutable 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 abtract 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.
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.AnalysisCache
— TypeAnalysisCache
JET's internal representation of a global analysis cache.
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.AnalyzerState
— Typemutable struct AnalyzerState
...
end
The mutable object that holds various states that are consumed by all AbstractAnalyzer
s.
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 JET configurations passed as keyword arguments jetconfigs
of the NewAnalyzer(; jetconfigs...)
constructor, and the constructed AnalyzerState
is usually kept within NewAnalyzer
itself:
function NewAnalyzer(; jetconfigs...)
...
state = AnalyzerState(; jetconfigs...)
return NewAnalyzer(..., state)
end
AnalyzerState(analyzer::NewAnalyzer) = analyzer.state
JET.BasicPass
— TypeThe basic (default) error analysis pass.
TODO: elaborate this documentation.
JET.ConcreteInterpreter
— TypeConcreteInterpreter
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 analysisJuliaInterpreter.evaluate_call_recurse!
to special caseinclude
callsJuliaInterpreter.handle_err
to wrap an error happened during interpretation intoActualErrorWrapped
JET.InferenceErrorReport
— TypeInferenceErrorReport
An interface type of error reports that JET collects by abstract interpration. In order for R<:InferenceErrorReport
to implement the interface, it should satisfy the following requirements:
Required fields
R
should have the following fields, which explains where and how this error is reported:vst::VirtualStackTrace
: a virtual stack trace of the errorsig::Signature
: a signature of the error point
Note that
R
can have additional fields other thanvst
andsig
to explain why this error is reported (mostly used forprint_report_message
).Required overloads
Optional overloads
R<:InferenceErrorReport
is supposed to be constructed using the following constructor
R(::AbstractAnalyzer, state, spec_args...) -> R
where state
can be either of:
state::Tuple{Union{Core.Compiler.InferenceState, Core.Compiler.OptimizationState}, Int64}
: a state with the current program counter specifiedstate::InferenceState
: a state with the current program counter set tostate.currpc
state::InferenceResult
: a state with the current program counter unknownstate::MethodInstance
: a state with the current program counter unknown
See also: @jetreport
, VirtualStackTrace
, VirtualFrame
JET.JETAnalyzer
— TypeEvery entry point of error analysis can accept any of general JET 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.
SeeBasicPass
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).
SeeSoundPass
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.
SeeTypoPass
for more details.
Note You can also set up your own analysis using JET's
AbstractAnalyzer
-Framework.
JET.JETCachedResult
— TypeJETCachedResult
JETResult
is transformed into JETCachedResult
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::JETCachedResult
.
InferenceErrorReport
s found within already-analyzed result::InferenceResult
can be accessed with get_cached_reports(analyzer, result)
.
JET.JETCallResult
— Typeres::JETCallResult
Represents the result of JET's analysis on a function call.
res.result::InferenceResult
: the result of this analysisres.analyzer::AbstractAnalyzer
:AbstractAnalyzer
used for this analysisres.source::String
: the identity key of this analysisres.jetconfigs
: JET configurations used for this analysis
JETCallResult
implements show
methods for each different frontend. An appropriate show
method will be automatically choosen and render the analysis result.
JET.JETResult
— TypeJETResult
analyzer::AbstractAnalyzer
manages InferenceErrorReport
associating it with InferenceResult
. InferenceErrorReport
s found within currently-analyzed result::InferenceResult
can be accessed with get_reports(analyzer, result)
.
JET.JETToplevelResult
— Typeres::JETToplevelResult
Represents the result of JET's analysis on a top-level script.
res.analyzer::AbstractAnalyzer
:AbstractAnalyzer
used for this analysisres.res::VirtualProcessResult
:VirtualProcessResult
collected from this analysisres.source::String
: the identity key of this analysisres.jetconfigs
: JET configurations used for this analysis
JETToplevelResult
implements show
methods for each different frontend. An appropriate show
method will be automatically choosen and render the analysis result.
JET.NativeRemark
— TypeNativeRemark <: InferenceErrorReport
This special InferenceErrorReport
wraps remarks by the default abstract interpretation. "remarks" are information that Julia's native compiler emits about how its type inference goes, and those remarks are less interesting in term of "error checking", so currently any of JET's pre-defined report passes doesn't make any use of NativeRemark
.
JET.OptAnalyzer
— TypeEvery entry point of optimization analysis can accept any of general JET 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 thisskip_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 ═════ ┌ @ REPL[2]:2 %45(Main.undef, n) │ runtime dispatch detected: %45::Type{Vector{_A}} where _A(Main.undef, n::Int64) └───────────── ┌ @ REPL[2]:3 Main.fill_twos!(%46) │ runtime dispatch detected: Main.fill_twos!(%46::Vector) └───────────── Vector (alias for Array{_A, 1} where _A) # we can get reports from non-concrete calls with `skip_noncompileable_calls=false` julia> @report_opt skip_noncompileable_calls=false strange_twos(3) ═════ 4 possible errors found ═════ ┌ @ REPL[2]:3 Main.fill_twos!(a) │┌ @ REPL[1]:3 a[%14] = 2 ││ runtime dispatch detected: Base.setindex!(a::Vector, 2, %14::Int64) │└───────────── │┌ @ REPL[1]:3 a[i] = 2 ││┌ @ array.jl:877 Base.convert(_, x) │││ runtime dispatch detected: Base.convert(_::Any, x::Int64) ││└──────────────── ┌ @ REPL[2]:2 %45(Main.undef, n) │ runtime dispatch detected: %45::Type{Vector{_A}} where _A(Main.undef, n::Int64) └───────────── ┌ @ REPL[2]:3 Main.fill_twos!(%46) │ runtime dispatch detected: Main.fill_twos!(%46::Vector) └───────────── Vector (alias for Array{_A, 1} where _A)
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@nospecialize
d arguments aren't fully concrete.skip_noncompileable_calls = true
also respects this behavior, i.e. doesn't skip compileable abstract calls:julia> function maybesin(@nospecialize x) if isa(x, Number) return sin(x) # this call is dynamically dispatched else return 0 end end maybesin (generic function with 1 method) julia> report_opt((Vector{Any},)) do xs for x in xs s = maybesin(x) # this call is resolved statically and compiled s !== 0 && return s end end ═════ 1 possible error found ═════ ┌ @ none:3 s = maybesin(x) │┌ @ none:3 sin(%3) ││ runtime dispatch detected: sin(%3::Number)::Any │└────────── julia> function maybesin(x) # now `maybesin` is always called with concrete `x` if isa(x, Number) return sin(x) # this call is dynamically dispatched else return 0 end end maybesin (generic function with 1 method) julia> report_opt((Vector{Any},)) do xs for x in xs s = maybesin(x) # this call is dynamically dispatched s !== 0 && return s end end ═════ 1 possible error found ═════ ┌ @ none:3 maybesin(%21) │ runtime dispatch detected: maybesin(%21::Any)::Any └──────────
function_filter = @nospecialize(ft)->true
:
A predicate which takes a function type and returnsfalse
to skip runtime dispatch analysis on the function call. This configuration is particularly useful when your program uses a function that is intentionally written to use runtime dispatch.# ignores `Core.Compiler.widenconst` calls (since it's designed to be runtime-dispatched): julia> function_filter(@nospecialize(ft)) = ft !== typeof(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 tothrow
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. Ifskip_unoptimized_throw_blocks
is set tofalse
, 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.PrintConfig
— TypeConfigurations for report printing. The configurations below will be active whenever show
ing JET's analysis result within REPL.
annotate_types::Bool = false
When set totrue
, annotates types when printing analyzed call stack.Examples:
- with
annotate_types = false
(default):julia> @report_call sum("julia") ═════ 2 possible errors found ═════ ┌ @ reduce.jl:549 Base.:(var"#sum#281")(pairs(NamedTuple()), #self#, a) │┌ @ reduce.jl:549 sum(identity, a) ││┌ @ reduce.jl:520 Base.:(var"#sum#280")(pairs(NamedTuple()), #self#, f, a) │││┌ @ reduce.jl:520 mapreduce(f, Base.add_sum, a) ││││┌ @ reduce.jl:294 Base.:(var"#mapreduce#277")(pairs(NamedTuple()), #self#, f, op, itr) │││││┌ @ reduce.jl:294 mapfoldl(f, op, itr) ││││││┌ @ reduce.jl:162 Base.:(var"#mapfoldl#273")(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 v = Base._foldl_impl(op, nt, itr) ││││││││││┌ @ reduce.jl:62 v = op(v, y[1]) │││││││││││┌ @ reduce.jl:81 op.rf(acc, x) ││││││││││││┌ @ reduce.jl:24 x + y │││││││││││││ no matching method found for `+(::Char, ::Char)`: (x::Char + y::Char)::Union{} ││││││││││││└──────────────── │││││││││┌ @ reduce.jl:49 Base.reduce_empty_iter(op, itr) ││││││││││┌ @ reduce.jl:370 Base.reduce_empty_iter(op, itr, Base.IteratorEltype(itr)) │││││││││││┌ @ reduce.jl:371 Base.reduce_empty(op, eltype(itr)) ││││││││││││┌ @ reduce.jl:347 Base.reduce_empty(op.rf, T) │││││││││││││┌ @ reduce.jl:339 Base.reduce_empty(+, T) ││││││││││││││┌ @ reduce.jl:330 zero(T) │││││││││││││││ no matching method found for `zero(::Type{Char})`: zero(T::Type{Char})::Union{} ││││││││││││││└─────────────────
- with
annotate_types = true
julia> @report_call annotate_types = true sum("julia") ═════ 2 possible errors found ═════ ┌ @ reduce.jl:549 Base.:(var"#sum#281")(pairs(NamedTuple()::NamedTuple{(), Tuple{}})::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, #self#::typeof(sum), a::String)::Union{} │┌ @ reduce.jl:549 sum(identity, a::String)::Union{} ││┌ @ reduce.jl:520 Base.:(var"#sum#280")(pairs(NamedTuple()::NamedTuple{(), Tuple{}})::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, #self#::typeof(sum), f::typeof(identity), a::String)::Union{} │││┌ @ reduce.jl:520 mapreduce(f::typeof(identity), Base.add_sum, a::String)::Union{} ││││┌ @ reduce.jl:294 Base.:(var"#mapreduce#277")(pairs(NamedTuple()::NamedTuple{(), Tuple{}})::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}}, #self#::typeof(mapreduce), f::typeof(identity), op::typeof(Base.add_sum), itr::String)::Union{} │││││┌ @ reduce.jl:294 mapfoldl(f::typeof(identity), op::typeof(Base.add_sum), itr::String)::Union{} ││││││┌ @ reduce.jl:162 Base.:(var"#mapfoldl#273")(Base._InitialValue()::Base._InitialValue, #self#::typeof(mapfoldl), f::typeof(identity), op::typeof(Base.add_sum), itr::String)::Union{} │││││││┌ @ reduce.jl:162 Base.mapfoldl_impl(f::typeof(identity), op::typeof(Base.add_sum), init::Base._InitialValue, itr::String)::Union{} ││││││││┌ @ reduce.jl:44 Base.foldl_impl(op′::Union{}, nt::Base._InitialValue, itr′::Union{})::Union{} │││││││││┌ @ reduce.jl:48 v = Base._foldl_impl(op::Base.BottomRF{typeof(Base.add_sum)}, nt::Base._InitialValue, itr::String)::Union{} ││││││││││┌ @ reduce.jl:62 v = op::Base.BottomRF{typeof(Base.add_sum)}(v::Char, (y::Tuple{Char, Int64})[1]::Char)::Union{} │││││││││││┌ @ reduce.jl:81 (op::Base.BottomRF{typeof(Base.add_sum)}).rf::typeof(Base.add_sum)(acc::Char, x::Char)::Union{} ││││││││││││┌ @ reduce.jl:24 (x::Char + y::Char)::Union{} │││││││││││││ no matching method found for `+(::Char, ::Char)`: (x::Char + y::Char)::Union{} ││││││││││││└──────────────── │││││││││┌ @ reduce.jl:49 Base.reduce_empty_iter(op::Base.BottomRF{typeof(Base.add_sum)}, itr::String)::Union{} ││││││││││┌ @ reduce.jl:370 Base.reduce_empty_iter(op::Base.BottomRF{typeof(Base.add_sum)}, itr::String, Base.IteratorEltype(itr::String)::Base.HasEltype)::Union{} │││││││││││┌ @ reduce.jl:371 Base.reduce_empty(op::Base.BottomRF{typeof(Base.add_sum)}, eltype(itr::String)::Type{Char})::Union{} ││││││││││││┌ @ reduce.jl:347 Base.reduce_empty((op::Base.BottomRF{typeof(Base.add_sum)}).rf::typeof(Base.add_sum), T::Type{Char})::Union{} │││││││││││││┌ @ reduce.jl:339 Base.reduce_empty(+, T::Type{Char})::Union{} ││││││││││││││┌ @ reduce.jl:330 zero(T::Type{Char})::Union{} │││││││││││││││ no matching method found for `zero(::Type{Char})`: zero(T::Type{Char})::Union{} ││││││││││││││└─────────────────
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)
- with
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.
JET.ReportPass
— Typeabstract 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 JET configurations (see the example documentation of AbstractAnalyzer
for details). 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.SeriousExceptionReport
— TypeSeriousExceptionReport <: 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.Signature
— TypeSignature
Represents an expression signature. print_signature
implements a frontend functionality to show this type.
JET.SoundPass
— TypeThe sound error analysis pass.
TODO: elaborate this documentation.
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 virtualize 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::Bool = 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.
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
,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 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 sequential top-level code execution. In virtual code execution, JET will selectively interpret "top-level definitions" (like a function definition) just like Julia's top-level code execution, while it tries to avoid executing any other parts of code like function calls and leaves them to succeeding static analysis by abstract interpretation.
However, currently, JET doesn't track the "inter-code-block" level code dependencies, and so the selective 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 (i.e. "concretized") but just left for abstract interpreation (i.e. "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 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 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. To put in other word, when JET sees a code that matches any of code patterns specified by an user, JET will try to interpret and concretize the code, regardless of whether JET's code selection logic decides to concretize it or not.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.Although configuring
concretization_patterns
properly could be really tricky, we can effectively debug JET's top-level code concretization plan using thetoplevel_logger
configuration with the logging level above than1
("debug") level. With thetoplevel_logger
configuration, we can see:- which code is matched with
concretization_patterns
and forcibly concretized - which code is selected to be concretized or not by JET's code selection logic: where
t
-annotated statements are concretized whilef
-annotated statements are abstracted and left abstract interpretation
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
.- 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.
JET.ToplevelErrorReport
— TypeToplevelErrorReport
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 contextline::Int
: the line number in the file containing the interpretation context
See also: virtual_process
, ConcreteInterpreter
JET.TypoPass
— TypeA typo detection pass.
TODO: elaborate this documentation.
JET.UncaughtExceptionReport
— TypeUncaughtExceptionReport <: InferenceErrorReport
Represents general throw
calls traced during inference. This is reported only when it's not caught by control flow.
JET.VirtualFrame
— TypeVirtualFrame
Stack information representing virtual execution context:
file::Symbol
: the path to the file containing the virtual execution contextline::Int
: the line number in the file containing the virtual execution contextsig::Signature
: a signature of this framelinfo::MethodInstance
: TheMethodInstance
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.VirtualProcessResult
— Typeres::VirtualProcessResult
res.included_files::Set{String}
: files that have been analyzedres.defined_modules::Set{Module}
: module contexts created while this top-level analysisres.toplevel_error_reports::Vector{ToplevelErrorReport}
: toplevel errors found during the text parsing or partial (actual) interpretation; these reports are "critical" and should have precedence overinference_error_reports
res.inference_error_reports::Vector{InferenceErrorReport}
: possible error reports found byAbstractAnalyzer
res.toplevel_signatures
: signatures of methods defined within the analyzed filesres.actual2virtual::Pair{Module, Module}
: keeps actual and virtual module
JET.VirtualStackTrace
— TypeVirtualStackTrace
Represents a virtual stack trace in the form of a vector of VirtualFrame
. The vector holds VirtualFrame
s 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.WatchConfig
— TypeConfigurations for "watch" mode. The configurations will only be active when used with report_and_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-performe analysis when you make a change to `Base` julia> report_and_watch_file(yourfile; revise_modules = [Base])
Core.Compiler.add_call_backedges!
— Methodadd_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_call
— Methodbail_out_call(analyzer::JETAnalyzer, ...)
With this overload, abstract_call_gf_by_type(analyzer::JETAnalyzer, ...)
doesn't bail out inference even after the current return type grows up to Any
and collects as much error points as possible. Of course this slows down inference performance, but hoopefully it stays to be "practical" speed since the number of matching methods are limited beforehand.
Core.Compiler.bail_out_toplevel_call
— Methodbail_out_toplevel_call(analyzer::AbstractAnalyzer, ...)
An overload for abstract_call_gf_by_type(analyzer::AbstractAnalyzer, ...)
, which keeps inference on non-concrete call sites in a toplevel frame created by virtual_process
.
Core.Compiler.const_prop_entry_heuristic
— Methodconst_prop_entry_heuristic(analyzer::JETAnalyzer, result::MethodCallResult, sv::InferenceState)
This overload for abstract_call_method_with_const_args(analyzer::JETAnalyzer, ...)
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_policy
— Methodinlining_policy(analyzer::AbstractAnalyzer, @nospecialize(src), ...) -> source::Any
Implements inlining policy for AbstractAnalyzer
. Since AbstractAnalyzer
works on InferenceResult
whose src
field keeps JETResult
or JETCachedResult
, this implementation needs to forward their wrapped source to inlining_policy(::AbstractInterpreter, ::Any, ::UInt8)
.
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".
JET.add_new_report!
— Methodadd_new_report!(analyzer::AbstractAnalyzer, result::InferenceResult, report::InferenceErrorReport)
Adds new report::InferenceErrorReport
associated with result::InferenceResult
.
JET.aggregation_policy
— Methodaggregation_policy(analyzer::AbstractAnalyzer)
Defines how analyzer
aggregates InferenceErrorReport
s. 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 InferenceErrorReport
s 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_task_parallel_code!
— Methodanalyze_task_parallel_code!(analyzer::AbstractAnalyzer, argtypes::Argtypes, 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
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.configured_reports
— MethodConfigurations 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 ═════
┌ @ REPL[1]:2 r1 = sum(a)
│┌ @ reduce.jl:549 Base.:(var"#sum#281")(pairs(NamedTuple()), #self#, a)
││┌ @ reduce.jl:549 sum(identity, a)
│││┌ @ reduce.jl:520 Base.:(var"#sum#280")(pairs(NamedTuple()), #self#, f, a)
││││┌ @ reduce.jl:520 mapreduce(f, Base.add_sum, a)
│││││┌ @ reduce.jl:294 Base.:(var"#mapreduce#277")(pairs(NamedTuple()), #self#, f, op, itr)
││││││┌ @ reduce.jl:294 mapfoldl(f, op, itr)
│││││││┌ @ reduce.jl:162 Base.:(var"#mapfoldl#273")(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 v = Base._foldl_impl(op, nt, itr)
│││││││││││┌ @ reduce.jl:62 v = op(v, y[1])
││││││││││││┌ @ reduce.jl:81 op.rf(acc, x)
│││││││││││││┌ @ reduce.jl:24 x + y
││││││││││││││ no matching method found for `+(::Char, ::Char)`: (x::Char + y::Char)::Union{}
│││││││││││││└────────────────
││││││││││┌ @ reduce.jl:49 Base.reduce_empty_iter(op, itr)
│││││││││││┌ @ reduce.jl:370 Base.reduce_empty_iter(op, itr, Base.IteratorEltype(itr))
││││││││││││┌ @ reduce.jl:371 Base.reduce_empty(op, eltype(itr))
│││││││││││││┌ @ reduce.jl:347 Base.reduce_empty(op.rf, T)
││││││││││││││┌ @ reduce.jl:339 Base.reduce_empty(+, T)
│││││││││││││││┌ @ reduce.jl:330 zero(T)
││││││││││││││││ no matching method found for `zero(::Type{Char})`: zero(T::Type{Char})::Union{}
│││││││││││││││└─────────────────
┌ @ REPL[1]:3 r2 = undefsum(a)
│ `undefsum` is not defined
└─────────────
# 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 ═════
┌ @ REPL[1]:3 r2 = undefsum(a)
│ `undefsum` is not defined
└─────────────
# 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 ═════
┌ @ REPL[1]:3 r2 = undefsum(a)
│ `undefsum` is not defined
└─────────────
JET.copy_report
— Methodcopy_report(report::R) where R<:InferenceErrorReport -> new::R
Returns new new::R
, that should be identical to the original report::R
, except that new.vst
is copied from report.vst
so that the further modifcation on report.vst
that may happen in later abstract interpretation doesn't affect new.vst
.
JET.islineage
— Methodislineage(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_file
— MethodJET.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:
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)"""
JET configurations specified as keyword arguments have precedence over those specified via a configuration file.
JET.partially_interpret!
— Methodpartially_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_message
— Methodprint_report_message(io::IO, report::R) where R<:InferenceErrorReport
Prints to io
and describes why report
is reported.
JET.print_signature
— Methodprint_signature(::R) where R<:InferenceErrorReport -> Bool
Configures whether or not to print the report signature when printing R
(defaults to true
).
JET.report_and_watch_file
— Methodreport_and_watch_file(filename::AbstractString;
jetconfigs...)
Watches filename
and keeps re-triggering analysis with report_file
on code update. JET will try to analyze all the include
d files reachable from filename
, and it will re-trigger analysis if there is code update detected in any of the include
d 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.
See also: report_file
JET.report_call
— Methodreport_call(f, [types];
analyzer::Type{<:AbstractAnalyzer} = JETAnalyzer,
jetconfigs...) -> JETCallResult
report_call(tt::Type{<:Tuple};
analyzer::Type{<:AbstractAnalyzer} = JETAnalyzer,
jetconfigs...) -> JETCallResult
Analyzes the generic function call with the given type signature with analyzer
. And finally returns the analysis result as JETCallResult
.
JET.report_color
— Methodreport_color(::R) where R<:InferenceErrorReport -> Symbol
Configures the color for R
(defaults to :red
).
JET.report_file
— Methodreport_file(filename::AbstractString;
jetconfigs...) -> JETToplevelResult
Analyzes filename
and returns JETToplevelResult
.
This function will look for .JET.toml
configuration file in the directory of filename
, and search up the file tree until any .JET.toml
is (or isn't) found. When found, the configurations specified in the file will be applied. See JET's configuration file for more details.
When you want to analyze your package, but any file actually using it isn't available, the analyze_from_definitions
option can be useful (see ToplevelConfig
's analyze_from_definitions
option).
For example, JET can analyze JET itself like below:
# from the root directory of JET.jl
julia> report_file("src/JET.jl";
analyze_from_definitions = true)
See also: report_package
This function will enable the toplevel_logger
configuration by default with the default logging level. You can still explicitly specify and configure it:
report_file(args...;
toplevel_logger = nothing, # suppress toplevel logger
jetconfigs...) # other configurations
See JET's top-level analysis configurations for more details.
JET.report_opt
— Methodreport_opt(f, [types]; jetconfigs...) -> JETCallResult
report_opt(tt::Type{<:Tuple}; jetconfigs...) -> JETCallResult
Analyzes the generic function call with the given type signature with the optimization analyzer, which collects optimization failures and runtime dispatches involved within the call stack.
JET.report_package
— Functionreport_package(package::Union{AbstractString,Module};
jetconfigs...) -> JETToplevelResult
Analyzes package
in the same way as report_file
with the special default configurations, which are especially tuned for package analysis (see below for details). package
can be either a Module
or a String
. In the latter case it must be the name of a package in your current environment.
This function configures analysis with the following configurations:
analyze_from_definitions = true
: allows JET to enter analysis without top-level call sites; this is useful for package analysis since a package itself usually has only definitions but not usages (i.e. call sites)concretization_patterns = [:(x_)]
: concretizes every top-level code in a givenpackage
; concretizations are generally preferred for successful analysis as far as they're cheap, and a package definition doesn't contain heavy computations in general cases
See ToplevelConfig
for more details.
report_package([io::IO = stdout];
jetconfigs...) -> res::ReportResult
Like above but analyzes the package of the current project.
See also: report_file
JET.report_text
— Methodreport_text(text::AbstractString,
filename::AbstractString = "top-level";
jetconfigs...) -> JETToplevelResult
Analyzes text
and returns JETToplevelResult
.
JET.test_call
— Methodtest_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(f, types; jetconfigs...
and tests that the generic function call f(args...)
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_opt
— Methodtest_opt(f, [types]; broken::Bool = false, skip::Bool = false, jetconfigs...)
test_opt(tt::Type{<:Tuple}; broken::Bool = false, skip::Bool = false, jetconfigs...)
Tests the generic function call with the given type signature is free from runtime dispatch. Except that it takes a type signature rather than a call expression, this function works in the same way as @test_opt
.
JET.virtual_process
— Methodvirtual_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 analyzedres.defined_modules::Set{Module}
: module contexts created while this top-level analysisres.toplevel_error_reports::Vector{ToplevelErrorReport}
: toplevel errors found during the text parsing or partial (actual) interpretation; these reports are "critical" and should have precedence overinference_error_reports
res.inference_error_reports::Vector{InferenceErrorReport}
: possible error reports found byAbstractAnalyzer
res.toplevel_signatures
: signatures of methods defined within the analyzed filesres.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
:
- if
blk
is a:module
expression, recusively enters analysis into an newly defined virtual module lower
sblk
into:thunk
expressionlwr
(macros are also expanded in this step)- if the context module is virtualized, replaces self-references of the original context module with virtualized one: see
fix_self_references
ConcreteInterpreter
partially interprets some statements inlwr
that should not be abstracted away (e.g. a:method
definition); see alsopartially_interpret!
- finally,
AbstractAnalyzer
analyzes the remaining statements by abstract interpretation
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_context
— Methodvirtualize_module_context(actual::Module)
HACK: Returns a module where the context of actual
is virtualized.
The virtualization will be done by 2 steps below:
- loads the module context of
actual
into a sandbox module, and export the whole context from there - 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
.
Currently this function relies on Base.names
, and thus it can't restore the using
ed names.
JET.@jetconfigurable
— Macro@jetconfigurable function config_func(args...; configurations...)
...
end
This macro asserts that there's no configuration naming conflict across the @jetconfigurable
functions so that a configuration for a @jetconfigurable
function doesn't affect the other @jetconfigurable
functions. This macro also adds a dummy splat keyword arguments (jetconfigs...
) to the function definition so that any configuration of other @jetconfigurable
functions can be passed on to it.
JET.@jetreport
— Macro@jetreport struct T <: InferenceErrorReport
...
end
An utilitiy 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 T <: InferenceErrorReport
is defined using @jetreport
, then T
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.@nospecs
— Macro@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.@report_call
— Macro@report_call [jetconfigs...] f(args...)
Evaluates the arguments to the function call, determines its types, and then calls report_call
on the resulting expression. As with @code_typed
and its family, any of JET configurations can be given as the optional arguments like this:
# reports `rand(::Type{Bool})` with `aggressive_constant_propagation` configuration turned off
julia> @report_call aggressive_constant_propagation=false rand(Bool)
JET.@report_opt
— Macro@report_opt [jetconfigs...] f(args...)
Evaluates the arguments to the function call, determines its types, and then calls report_opt
on the resulting expression. As with @code_typed
and its family, any of JET configurations or optimization analysis specific configurations can be given as the optional arguments like this:
# reports `rand(::Type{Bool})` with `unoptimize_throw_blocks` configuration turned on
julia> @report_opt unoptimize_throw_blocks=true rand(Bool)
JET.@test_call
— Macro@test_call [jetconfigs...] [broken=false] [skip=false] f(args...)
Runs @report_call jetconfigs... f(args...)
and tests that the generic function call f(args...)
is free from problems that @report_call
can detect. If executed inside @testset
, returns a Pass
result if it is, a Fail
result if it contains any error points detected, or an Error
result if this macro encounters an unexpected error. When the test Fail
s, abstract call stack to each problem location will also be printed to stdout
.
julia> @test_call sincos(10)
Test Passed
Expression: #= none:1 =# JET.@test_call sincos(10)
As with @report_call
, any of JET configurations or analyzer specific configurations can be given as the optional arguments jetconfigs...
like this:
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. It means, the result of @test_call
will be included in the final @testset
summary, it supports skip
and broken
annotations as like @test
and its family:
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[9]:3
Expression: #= REPL[9]:3 =# JET.@test_call f(ref)
═════ 1 possible error found ═════
┌ @ REPL[7]:1 sin(ref[])
│ no matching method found for `sin(::Nothing)` (1/2 union split): sin((ref::Base.RefValue{Union{Nothing, Int64}})[]::Union{Nothing, Int64})::Union{}
└─────────────
Test Summary: | Pass Fail Broken Total
check errors | 1 1 1 3
ERROR: Some tests did not pass: 1 passed, 1 failed, 0 errored, 1 broken.
JET.@test_opt
— Macro@test_opt [jetconfigs...] [broken=false] [skip=false] f(args...)
Tests the generic function call f(args...)
is free from runtime dispatch. Returns a Pass
result if it is, a Fail
result if if contains any location where runtime dispatch or optimization failure happens, or an Error
result if this macro encounters an unexpected error. When the test Fail
s, abstract call stack to each problem location will also be printed to stdout
.
julia> @test_opt sincos(10)
Test Passed
Expression: #= none:1 =# JET.@test_opt sincos(10)
As with @report_opt
, any of JET configurations or optimization analysis specific configurations can be given as the optional arguments like this:
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 Test
standard library. See @test_call
for the details.
JET.@withmixedhash
— Macro@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