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.

Warning

The APIs described on this page are highly experimental and subject to change. This documentation is also a work in progress.

Interfaces

JET.AbstractAnalyzerType
abstract type AbstractAnalyzer <: AbstractInterpreter end

An 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

  1. JETInterface.AnalyzerState(::NewAnalyzer) -> AnalyzerState

    Return the AnalyzerState associated with the analyzer.

  2. JETInterface.AbstractAnalyzer(::NewAnalyzer, ::AnalyzerState) -> NewAnalyzer

    Construct a new NewAnalyzer from an existing analyzer and a replacement state. JET calls this method when it recreates an analyzer during top-level analysis or abstract interpretation.

  3. JETInterface.AnalysisToken(::NewAnalyzer) -> AnalysisToken

    Return the AnalysisToken that 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_token
source
JET.AnalyzerStateType
mutable struct AnalyzerState
    ...
end

Mutable storage for the state used by an AbstractAnalyzer.


JETInterface.AnalyzerState(analyzer::AbstractAnalyzer) -> AnalyzerState

Every 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.state
source
JET.AnalysisTokenMethod
JETInterface.AnalysisToken(analyzer::AbstractAnalyzer) -> AnalysisToken

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

source
JET.ToplevelAbstractAnalyzerType
abstract type ToplevelAbstractAnalyzer <: AbstractAnalyzer end

An 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 (const statements)
  • 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

source
JET.valid_configurationsFunction
JETInterface.valid_configurations(analyzer::AbstractAnalyzer) -> names or nothing

Return an iterable of Symbols naming the configurations accepted by analyzer. Return nothing to skip configuration-name validation.

source
JET.aggregation_policyFunction
JETInterface.aggregation_policy(analyzer::AbstractAnalyzer) -> key_function

Return a callable that maps each InferenceErrorReport to the key used to deduplicate reports. The default is default_aggregation_policy.


default_aggregation_policy(report::InferenceErrorReport) -> DefaultReportIdentity

Return the default deduplication key for a report. Two reports have the same key when they have:

  1. The same concrete report type
  2. Equal expression Signatures
  3. 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.

source
JET.typeinf_worldFunction
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.

source
JET.VSCode.vscode_diagnostics_orderFunction
vscode_diagnostics_order(analyzer::AbstractAnalyzer) -> Bool

If true (default) a diagnostic will be reported at entry site. Otherwise it's reported at error point.

source
JET.InferenceErrorReportMethod
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 point
  • sig::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

Optional methods

Construction

JET provides the following generic constructor:

Report(state, spec_args...) -> Report

state may be any of:

  • state::StateAtPC: a state with an explicitly specified program counter
  • state::InferenceState: a state using state.currpc as the program counter
  • state::InferenceResult: a state whose program counter is unknown
  • state::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...) -> Report

A manually defined report type must provide the storage constructor. @jetreport generates it automatically.

See also VirtualStackTrace and VirtualFrame.

source
JET.copy_reportFunction
JETInterface.copy_report(orig::Report) where Report<:InferenceErrorReport -> new::Report

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

source
JET.print_reportFunction
print_report(io::IO, report::ToplevelErrorReport)

Prints a report of the top-level error report to the given io.

source
JET.print_report_messageFunction
JETInterface.print_report_message(io::IO, report::Report) where Report<:InferenceErrorReport

Print to io a message explaining why report was emitted.

source
JET.print_signatureFunction
JETInterface.print_signature(::Report) where Report<:InferenceErrorReport -> Bool

Return whether to print the report's signature. The default is true.

source
JET.report_colorFunction
JETInterface.report_color(::Report) where Report<:InferenceErrorReport -> Symbol

Return the color used to print the report. The default is ERROR_COLOR (:light_red).

source
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...) -> JETCallResult

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

source
JET.call_test_exFunction
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.

source
JET.func_testFunction
func_test(func, testname::Symbol, args...; broken::Bool = false, skip::Bool = false, jetconfigs...)

An internal utility for implementing functions similar to test_call.

source
JET.analyze_and_report_file!Function
analyze_and_report_file!(interp::ConcreteInterpreter,
                         filename::AbstractString,
                         pkgid::Union{Nothing,PkgId} = nothing;
                         jetconfigs...) -> JETToplevelResult

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

source
JET.analyze_and_report_package!Function
analyze_and_report_package!(analyzer::AbstractAnalyzer, pkgmod::Module;
                            jetconfigs...) -> JETToplevelResult

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

source
JET.analyze_and_report_text!Function
analyze_and_report_text!(interp::ConcreteInterpreter,
                         text::AbstractString,
                         filename::AbstractString = "top-level",
                         pkgid::Union{Nothing,PkgId} = nothing;
                         jetconfigs...) -> JETToplevelResult

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

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

source
JET.@jetreportMacro
@jetreport struct NewReport <: InferenceErrorReport
    ...
end

Define an InferenceErrorReport subtype from its report-specific fields. The macro adds:

  • the required vst::VirtualStackTrace and sig::Signature fields
  • a storage constructor accepting vst, sig, and the report-specific fields
  • a copy_report method that copies vst and 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
end

Given sv::InferenceState and atype, construct this simplified report with MethodErrorReport(sv, atype, 0).

source

Examples