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.
In this documentation we will try to elaborate the framework APIs and showcase example analyzers.
The APIs described in this page is very experimental and subject to changes. And this documentation is also very WIP.
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 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 implement the following interfaces:
Required interfaces
JETInterface.AnalyzerState(analyzer::NewAnalyzer) -> AnalyzerState: Returns theAnalyzerStateforanalyzer::NewAnalyzer.JETInterface.AbstractAnalyzer(analyzer::NewAnalyzer, state::AnalyzerState) -> NewAnalyzer: Constructs a newNewAnalyzerinstance in the middle of JET's top-level analysis or abstract interpretation, given the previousanalyzer::NewAnalyzerandstate::AnalyzerState.JETInterface.AnalysisToken(analyzer::NewAnalyzer) -> AnalysisToken: Returns a uniqueAnalysisTokenobject used foranalyzer::NewAnalyzer.
See also AnalyzerState and AnalysisToken.
Example
JET.jl defines its default error analyzer BasicJETAnalyzer <: AbstractAnalyzer as the following (modified a bit for the sake of simplicity):
# 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
...
endThe mutable object that holds various states that are consumed by all AbstractAnalyzers.
JETInterface.AnalyzerState(analyzer::AbstractAnalyzer) -> AnalyzerStateIf NewAnalyzer implements the AbstractAnalyzer interface, NewAnalyzer should implement this AnalyzerState(analyzer::NewAnalyzer) -> AnalyzerState interface.
A new AnalyzerState is supposed to be constructed using the general configurations passed as keyword arguments jetconfigs of the NewAnalyzer(; jetconfigs...) constructor, and the constructed AnalyzerState is usually kept within NewAnalyzer 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) -> AnalysisTokenReturns AnalysisToken for the given analyzer::AbstractAnalyzer. AbstractAnalyzer instances can share the same cache if they perform the same analysis, otherwise their cache should be separated.
If NewAnalyzer implements the AbstractAnalyzer interface, it must implement this function to return a consistent token for instances that should share the same cache.
JET.ToplevelAbstractAnalyzer — Type
abstract type ToplevelAbstractAnalyzer <: AbstractAnalyzer endA specialized interface type for analyzers that perform top-level analysis of Julia code.
ToplevelAbstractAnalyzer extends AbstractAnalyzer to provide clear separation between analyzers that support top-level analysis and those that don't, offering several key architectural benefits:
- Type Safety: Only analyzers that explicitly extend
ToplevelAbstractAnalyzercan be used with JET'svirtual_processsystem, making it clear at compile time which analyzers support top-level analysis capabilities. - Responsibility Separation: Analyzers like
OptAnalyzerthat don't perform top-level analysis are no longer required to handle top-level specific code paths, reducing complexity and improving performance.
Top-Level Analysis Capabilities
ToplevelAbstractAnalyzer provides specialized functionality for analyzing top-level Julia constructs such as:
- Global variable assignments
- Constant declarations (
conststatements) - Module definitions and imports
- Method definitions at the top level
- Package-level code execution
This analyzer type is used in virtual_process system to analyze Julia code as it would be executed at the top level, handling both concrete interpretation of some statements and abstract interpretation of others.
Usage
ToplevelAbstractAnalyzer is typically used through JET's virtual process system:
# Create a concrete interpreter with a toplevel 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 the interfaces required by AbstractAnalyzer, and will automatically inherit the specialized top-level analysis behaviors provided by this type.
See Also
AbstractAnalyzer: The base analyzer interfaceJETAnalyzer: JET's default error analyzer that implements this interfacevirtual_process: The main function that uses this analyzer typeConcreteInterpreter: The concrete interpreter that works with this analyzer
JET.valid_configurations — Function
JETInterface.valid_configurations(analyzer::AbstractAnalyzer) -> names or nothingReturns a set of names that are valid as a configuration for analyzer. names should be an iterator of Symbol. No validations are performed if nothing is returned.
JET.aggregation_policy — Function
JETInterface.aggregation_policy(analyzer::AbstractAnalyzer)Defines how analyzer aggregates InferenceErrorReports. This policy determines how duplicate or similar reports are identified and grouped. Defaults to default_aggregation_policy.
default_aggregation_policy(report::InferenceErrorReport) -> DefaultReportIdentityReturns the default identity of report::InferenceErrorReport using DefaultReportIdentity, which aggregates reports based on their "error location".
DefaultReportIdentity aggregates InferenceErrorReports by creating an identity based on:
- The report type
- The signature of the method where the error was found
- The file and line number where the error occurred
This approach ignores the specific MethodInstance identity, allowing errors to be aggregated if they occur at the same file and line, under the assumption that errors at the same location are likely duplicates even if in different method specializations.
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
InferenceErrorReportIn order for Report <: InferenceErrorReport to implement the interface, it should satisfy the following requirements:
Required fields
Reportshould 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
Reportcan have additional fields other thanvstandsigto explain why this error is reported (mostly used forprint_report_message).Required overloads
Optional overloads
Report <: InferenceErrorReport is supposed to be constructed using the following constructor
Report(::AbstractAnalyzer, state, spec_args...) -> Reportwhere state can be either of:
state::Tuple{Union{Compiler.InferenceState, Compiler.OptimizationState}, Int64}: a state with the current program counter specifiedstate::InferenceState: a state with the current program counter set tostate.currpcstate::InferenceResult: a state with the current program counter unknownstate::MethodInstance: a state with the current program counter unknown
See also: @jetreport, VirtualStackTrace, 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::ReportReturns new new::Report, that should be identical to the original orig::Report, except that new.vst is copied from orig.vst so that the further modification on orig.vst that may happen in later abstract interpretation doesn't affect new.vst.
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<:InferenceErrorReportPrints to io and describes why report is reported.
JET.print_signature — Function
JETInterface.print_signature(::Report) where Report<:InferenceErrorReport -> BoolConfigures whether or not to print the report signature when printing Report (defaults to true).
JET.report_color — Function
JETInterface.report_color(::Report) where Report<:InferenceErrorReport -> SymbolConfigures the color for Report (defaults to :red).
JET.analyze_and_report_call! — Function
analyze_and_report_call!(analyzer::AbstractAnalyzer, f, [types]; jetconfigs...) -> JETCallResult
analyze_and_report_call!(analyzer::AbstractAnalyzer, tt::Type{<:Tuple}; jetconfigs...) -> JETCallResult
analyze_and_report_call!(analyzer::AbstractAnalyzer, mi::MethodInstance; jetconfigs...) -> JETCallResultA generic entry point to analyze a function call with AbstractAnalyzer. Finally returns the analysis result as JETCallResult. Note that this is intended to be used by developers of AbstractAnalyzer only. General users should use high-level entry points like 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...; jetconfigs...)An internal utility function to implement a test_call-like function. See the implementation of test_call.
JET.analyze_and_report_file! — Function
analyze_and_report_file!(interp::ConcreteInterpreter, filename::AbstractString; jetconfigs...) -> JETToplevelResultA generic entry point to analyze a file with interp::ConcreteInterpreter. Finally returns the analysis result as JETToplevelResult. Note that this is intended to be used by developers of AbstractAnalyzer and ConcreteInterpreter only. General users should use high-level entry points like report_file.
JET.analyze_and_report_package! — Function
analyze_and_report_package!(analyzer::AbstractAnalyzer, package::Module; jetconfigs...) -> JETToplevelResultA generic entry point to analyze a package with analyzer::AbstractAnalyzer. Finally returns the analysis result as JETToplevelResult. Note that this is intended to be used by developers of AbstractAnalyzer only. General users should use high-level entry points like report_package.
JET.analyze_and_report_text! — Function
analyze_and_report_text!(interp::ConcreteInterpreter, text::AbstractString,
filename::AbstractString = "top-level";
jetconfigs...) -> JETToplevelResultA generic entry point to analyze a top-level code with interp::ConcreteInterpreter. Finally returns the analysis result as JETToplevelResult. Note that this is intended to be used by developers of AbstractAnalyzer and ConcreteInterpreter only. General users should use high-level entry points like report_text.
JET.add_new_report! — Function
add_new_report!(analyzer::AbstractAnalyzer, result::InferenceResult, report::InferenceErrorReport)Adds a new error report to the analyzer's collection for a specific inference result.
This function associates an InferenceErrorReport with its corresponding result::InferenceResult in the analyzer's internal storage. The report becomes part of the analysis results that can be retrieved later using get_reports(analyzer, result).
Reports are stored in the order they are added, which can be important for maintaining the logical sequence of errors discovered during analysis.
JET.@jetreport — Macro
@jetreport struct NewReport <: InferenceErrorReport
...
endA utility 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 NewReport <: InferenceErrorReport is defined using @jetreport, then NewReport 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
endand constructed as like MethodErrorReport(sv::InferenceState, atype::Any, 0).