AbstractAnalyzer framework
JET offers an infrastructure to implement a "plugin" code analyzer. Actually, JET's default error analyzer is one specific instance of such a plugin analyzer built on top of the framework.
This documentation elaborates on the framework APIs and showcases example analyzers.
The APIs described on this page are highly experimental and subject to change. This documentation is also a work in progress.
Interfaces
JET.JETInterface — Module
JETInterfaceThis baremodule exports names that form the APIs of AbstractAnalyzer framework. using JET.JETInterface loads all names that are necessary to define a plugin analysis.
JET.AbstractAnalyzer — Type
abstract type AbstractAnalyzer <: AbstractInterpreter endAn interface type for analyzers built on JET's AbstractAnalyzer framework.
To implement the AbstractAnalyzer interface, declare NewAnalyzer as a subtype of AbstractAnalyzer and implement the following methods:
Required interfaces
JETInterface.AnalyzerState(::NewAnalyzer) -> AnalyzerStateReturn the
AnalyzerStateassociated with the analyzer.JETInterface.AbstractAnalyzer(::NewAnalyzer, ::AnalyzerState) -> NewAnalyzerConstruct a new
NewAnalyzerfrom an existing analyzer and a replacement state. JET calls this method when it recreates an analyzer during top-level analysis or abstract interpretation.JETInterface.AnalysisToken(::NewAnalyzer) -> AnalysisTokenReturn the
AnalysisTokenthat identifies analyzer instances whose cached analysis results may be shared.
See also AnalyzerState and AnalysisToken.
Example
JET.jl's default error analyzer, BasicJETAnalyzer <: AbstractAnalyzer, can be represented by the following simplified definition:
# the default error analyzer for JET.jl
struct BasicJETAnalyzer <: AbstractAnalyzer
state::AnalyzerState
analysis_token::AnalysisToken
# ... other fields
end
# AbstractAnalyzer API requirements
JETInterface.AnalyzerState(analyzer::BasicJETAnalyzer) = analyzer.state
JETInterface.AbstractAnalyzer(
analyzer::BasicJETAnalyzer, state::AnalyzerState) =
BasicJETAnalyzer(state, analyzer.analysis_token, ...)
JETInterface.AnalysisToken(analyzer::BasicJETAnalyzer) = analyzer.analysis_tokenJET.AnalyzerState — Type
mutable struct AnalyzerState
...
endMutable storage for the state used by an AbstractAnalyzer.
JETInterface.AnalyzerState(analyzer::AbstractAnalyzer) -> AnalyzerStateEvery NewAnalyzer subtype must implement this method to return its AnalyzerState.
A new AnalyzerState is normally constructed by the NewAnalyzer(; jetconfigs...) constructor and stored in the analyzer itself:
function NewAnalyzer(world::UInt = Base.get_world_counter(); jetconfigs...)
...
state = AnalyzerState(world; jetconfigs...)
return NewAnalyzer(..., state)
end
JETInterface.AnalyzerState(analyzer::NewAnalyzer) = analyzer.stateJET.AnalysisToken — Method
JETInterface.AnalysisToken(analyzer::AbstractAnalyzer) -> AnalysisTokenReturn the AnalysisToken used as the cache owner for analyzer.
Every NewAnalyzer subtype must implement this method. Instances that can safely reuse cached inference and report results must return the same token object. Instances whose analysis behavior or configuration can produce incompatible results must return distinct token objects.
JET.ToplevelAbstractAnalyzer — Type
abstract type ToplevelAbstractAnalyzer <: AbstractAnalyzer endAn interface type for analyzers that support top-level analysis of Julia code.
ToplevelAbstractAnalyzer is a subtype of AbstractAnalyzer. Methods specialized for this type provide top-level-specific inference behavior and distinguish these analyzers from analyzers such as OptAnalyzer, which do not support top-level analysis.
Top-level analysis capabilities
Together with a ConcreteInterpreter, a ToplevelAbstractAnalyzer supports analysis of top-level constructs such as:
- Global variable assignments
- Constant declarations (
conststatements) - Module definitions and imports
- Method definitions at the top level
- Package-level code execution
The virtual_process system uses the concrete interpreter and analyzer to process Julia code as it would execute at the top level. Some statements are interpreted concretely, while others are analyzed abstractly.
Usage
ToplevelAbstractAnalyzer is typically used through JET's virtual process system:
# Create a concrete interpreter with a top-level analyzer
interp = JETConcreteInterpreter(JETAnalyzer())
analyzer = ToplevelAbstractAnalyzer(interp)
# Analyze top-level code
result = analyze_and_report_text!(interp, "x = 1; y = x + 1")Implementation requirements
Concrete subtypes of ToplevelAbstractAnalyzer must implement all interfaces required by AbstractAnalyzer. Methods specialized for this interface then provide the top-level-specific abstract interpretation behavior.
See also
AbstractAnalyzer: the base analyzer interfaceJETAnalyzer: JET's default error analyzer for this interfacevirtual_process: the top-level processing entry pointConcreteInterpreter: the corresponding concrete interpreter
JET.valid_configurations — Function
JETInterface.valid_configurations(analyzer::AbstractAnalyzer) -> names or nothingReturn an iterable of Symbols naming the configurations accepted by analyzer. Return nothing to skip configuration-name validation.
JET.aggregation_policy — Function
JETInterface.aggregation_policy(analyzer::AbstractAnalyzer) -> key_functionReturn a callable that maps each InferenceErrorReport to the key used to deduplicate reports. The default is default_aggregation_policy.
default_aggregation_policy(report::InferenceErrorReport) -> DefaultReportIdentityReturn the default deduplication key for a report. Two reports have the same key when they have:
- The same concrete report type
- Equal expression
Signatures - The same file and line in their final
VirtualFrames
Signature equality compares only the _sig elements, using ===, and ignores tt. The stack-trace component uses only the final frame's file and line; it omits that frame's MethodInstance and all preceding frames.
JET.typeinf_world — Function
typeinf_world(analyzer::AbstractAnalyzer) -> world::Union{UInt,Nothing}Return the world age to use for type inference performed by the given analyzer, or nothing to use the current world.
When a specific world age is returned, the analyzer will invoke type inference within that fixed world using Base.invoke_in_world. This makes the analysis implementation more robust against potential invalidations that may be caused by loading external packages.
The default implementation returns nothing, meaning type inference runs in the latest world. Specific analyzer implementations may override this to return a fixed world age for stability.
JET.VSCode.vscode_diagnostics_order — Function
vscode_diagnostics_order(analyzer::AbstractAnalyzer) -> BoolIf true (default) a diagnostic will be reported at entry site. Otherwise it's reported at error point.
JET.InferenceErrorReport — Method
InferenceErrorReport()A concrete Report <: InferenceErrorReport must satisfy the following requirements.
Required fields
vst::VirtualStackTrace: the virtual stack trace from the analysis entry point to the error pointsig::Signature: the expression signature at the error point
A report may have additional fields used by print_report_message to explain why it was emitted.
Required methods
JETInterface.copy_report(report::Report) -> new::ReportJETInterface.print_report_message(io::IO, report::Report)
Optional methods
JETInterface.print_signature(report::Report) -> BoolJETInterface.report_color(report::Report) -> Symbol
Construction
JET provides the following generic constructor:
Report(state, spec_args...) -> Reportstate may be any of:
state::StateAtPC: a state with an explicitly specified program counterstate::InferenceState: a state usingstate.currpcas the program counterstate::InferenceResult: a state whose program counter is unknownstate::MethodInstance: a state whose program counter is unknown
The generic constructor derives vst and sig, then calls this storage constructor:
Report(vst::VirtualStackTrace, sig::Signature, spec_args...) -> ReportA manually defined report type must provide the storage constructor. @jetreport generates it automatically.
See also VirtualStackTrace and VirtualFrame.
JET.ToplevelErrorReport — Method
ToplevelErrorReport()In order for Report <: ToplevelErrorReport to implement the interface, it should satisfy the following requirements:
Required fields
Reportshould have the following fields:file::String: the filename of this errorline::Int: the line number of this error
Required overloads
JET.copy_report — Function
JETInterface.copy_report(orig::Report) where Report<:InferenceErrorReport -> new::ReportReturn a new Report equivalent to orig. Preserve every field except vst, which must contain the same frames as orig.vst in a distinct vector. Mutating either stack trace must not affect the other report.
JET.print_report — Function
print_report(io::IO, report::ToplevelErrorReport)Prints a report of the top-level error report to the given io.
JET.print_report_message — Function
JETInterface.print_report_message(io::IO, report::Report) where Report<:InferenceErrorReportPrint to io a message explaining why report was emitted.
JET.print_signature — Function
JETInterface.print_signature(::Report) where Report<:InferenceErrorReport -> BoolReturn whether to print the report's signature. The default is true.
JET.report_color — Function
JETInterface.report_color(::Report) where Report<:InferenceErrorReport -> SymbolReturn the color used to print the report. The default is ERROR_COLOR (:light_red).
JET.analyze_and_report_call! — Function
analyze_and_report_call!(analyzer::AbstractAnalyzer, f,
types = Base.default_tt(f);
jetconfigs...) -> JETCallResult
analyze_and_report_call!(analyzer::AbstractAnalyzer,
tt::Type{<:Tuple};
jetconfigs...) -> JETCallResult
analyze_and_report_call!(analyzer::AbstractAnalyzer,
mi::MethodInstance;
jetconfigs...) -> JETCallResultAnalyze a function call with analyzer and return the analysis result as a JETCallResult. This generic entry point is intended only for developers of AbstractAnalyzer. General users should use high-level entry points such as report_call and report_opt.
JET.call_test_ex — Function
call_test_ex(funcname::Symbol, testname::Symbol, ex0, __module__, __source__)An internal utility function to implement a @test_call-like macro. See the implementation of @test_call.
JET.func_test — Function
func_test(func, testname::Symbol, args...; broken::Bool = false, skip::Bool = false, jetconfigs...)An internal utility for implementing functions similar to test_call.
JET.analyze_and_report_file! — Function
analyze_and_report_file!(interp::ConcreteInterpreter,
filename::AbstractString,
pkgid::Union{Nothing,PkgId} = nothing;
jetconfigs...) -> JETToplevelResultAnalyze a file with interp and return the analysis result as a JETToplevelResult. This generic entry point is intended only for developers of AbstractAnalyzer and ConcreteInterpreter. General users should use high-level entry points such as report_file.
JET.analyze_and_report_package! — Function
analyze_and_report_package!(analyzer::AbstractAnalyzer, pkgmod::Module;
jetconfigs...) -> JETToplevelResultAnalyze the package module pkgmod with analyzer and return the analysis result as a JETToplevelResult. This generic entry point is intended only for developers of AbstractAnalyzer. General users should use high-level entry points such as report_package.
JET.analyze_and_report_text! — Function
analyze_and_report_text!(interp::ConcreteInterpreter,
text::AbstractString,
filename::AbstractString = "top-level",
pkgid::Union{Nothing,PkgId} = nothing;
jetconfigs...) -> JETToplevelResultAnalyze top-level text with interp and return the analysis result as a JETToplevelResult. This generic entry point is intended only for developers of AbstractAnalyzer and ConcreteInterpreter. General users should use high-level entry points such as report_text.
JET.add_new_report! — Function
add_new_report!(analyzer::AbstractAnalyzer, result::InferenceResult, report::InferenceErrorReport)Append report to the reports associated with result in analyzer, then return report. Retrieve the collection with get_reports(analyzer, result). Reports remain in insertion order.
JET.@jetreport — Macro
@jetreport struct NewReport <: InferenceErrorReport
...
endDefine an InferenceErrorReport subtype from its report-specific fields. The macro adds:
- the required
vst::VirtualStackTraceandsig::Signaturefields - a storage constructor accepting
vst,sig, and the report-specific fields - a
copy_reportmethod that copiesvstand preserves the other fields
The generic NewReport(state, spec_args...) constructor initializes vst and sig. A report defined with @jetreport only needs to implement the print_report_message interface; it may also override the optional report interfaces.
The following is a simplified version of JETAnalyzer's MethodErrorReport; the actual definition has additional behavior:
@jetreport struct MethodErrorReport <: InferenceErrorReport
@nospecialize t # ::Union{Type,Vector{Any}}
union_split::Int
end
function print_report_message(io::IO, (; t, union_split)::MethodErrorReport)
print(io, "no matching method found ")
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
endGiven sv::InferenceState and atype, construct this simplified report with MethodErrorReport(sv, atype, 0).