Internals of JET.jl

Abstract interpretation

In order to perform type-level program analysis, JET.jl uses the Compiler.AbstractInterpreter interface and customizes its abstract interpretation by overloading a subset of the Compiler functions, which were originally developed for the Julia compiler's type inference and for optimizations that aim at generating efficient native code for CPU execution.

JET.AbstractAnalyzer overloads a subset of Compiler methods to implement JET's core functionality, including interprocedural propagation of error reports and caching of analysis results. Each plugin analyzer, such as JET.JETAnalyzer, overloads additional Compiler methods to implement its own analysis on top of the AbstractAnalyzer infrastructure.

Most of these overloads use invoke to call the corresponding methods for AbstractInterpreter. The actual AbstractAnalyzer instance is still passed as the interpreter argument, so calls made from within those original methods can dispatch back to analyzer-specific overloads.

How AbstractAnalyzer manages caches

JET.AnalysisResultType
AnalysisResult

Container for error reports collected during analysis of a specific InferenceResult.

AbstractAnalyzer manages InferenceErrorReport instances by associating them with their corresponding InferenceResult. Reports found during the analysis of result::InferenceResult can be accessed via get_reports(analyzer, result).

source
JET.CachedAnalysisResultType
CachedAnalysisResult

A cached collection of reports associated with an AnalysisResult.

JET copies reports into this container and stacks it directly on the corresponding InferenceResult with CC.stack_analysis_result!. When that inference result is cached as a CodeInstance, the metadata is retained. Global cache lookup recovers the reports by traversing the CodeInstance with CC.traverse_analysis_results; local cache lookup can traverse the InferenceResult directly.

source
JET.AnalysisTokenType
mutable struct AnalysisToken
    AnalysisToken() = new()
end

An identity token used as the compiler cache owner for an AbstractAnalyzer.

The token's object identity determines which analyzer instances can reuse cached inference and report results. Reuse the same token object only for analyzers with cache-compatible behavior; use distinct objects otherwise.

source

Top-level analysis

JET.virtual_processFunction
virtual_process(interp::ConcreteInterpreter,
                x::Union{AbstractString,JS.SyntaxNode},
                filename::AbstractString,
                config::ToplevelConfig;
                overrideex::Union{Nothing,Expr}=nothing) -> res::VirtualProcessResult

Simulates Julia's top-level execution, collects error reports, and returns a VirtualProcessResult.

If x is an AbstractString, this function first parses it into a JS.SyntaxNode. The internal overrideex keyword may be used only when x is a JS.SyntaxNode, and its value must be an Expr with head :toplevel. The expression is analyzed in place of the syntax represented by x. AbstractString input does not accept this override.

The function processes each top-level code block (blk) in the resulting or supplied syntax tree as follows:

  1. If blk is or expands to a :module expression, define the module and recursively analyze its body in that new module.
  2. Otherwise, expand macros and lower blk. Skip literal results; a :thunk result supplies the lowered CodeInfo.
  3. If the context module was virtualized, rewrite self-references from the original module to the virtual module; see fix_self_references!.
  4. Use ConcreteInterpreter to concretely interpret statements that must not be abstracted, such as :method definitions; see partially_interpret!.
  5. Use ToplevelAbstractAnalyzer to analyze the remaining statements abstractly.
Warning

To process top-level code sequentially, as the Julia runtime does, virtual_process splits the input into code blocks and simulates them one at a time. This approach does not track concrete-value dependencies between separately processed blocks. Consequently, partial interpretation of a top-level definition can fail when it needs a global value defined in another block that was abstractly rather than concretely interpreted. Use concretization_patterns to force the relevant blocks to be concretely interpreted. See ToplevelConfig for details.

source
JET.VirtualProcessResultType
res::VirtualProcessResult
  • res.analyzed_files::Dict{String,AnalyzedFileInfo}: analyzed files and the source ranges associated with each module in those files.
  • res.toplevel_error_reports::Vector{ToplevelErrorReport}: reports produced during top-level processing, including parsing, macro expansion, lowering, and partial concrete interpretation. These critical reports take precedence over inference_error_reports.
  • res.inference_error_reports::Vector{InferenceErrorReport}: reports of potential errors found by ToplevelAbstractAnalyzer.
  • res.signature_infos::Vector{SignatureInfo}: method signatures collected for analysis from top-level definitions.
  • res.actual2virtual::Union{Actual2Virtual,Nothing}: maps the actual root module to its virtual counterpart, or is nothing when module virtualization is disabled.
source
JET.virtualize_module_contextFunction
virtualize_module_context(actual::Module)

Return a fresh virtual module that provides access to the bindings of actual.

Virtualization proceeds in two steps:

  1. Use using to make the defined names of actual available in a sandbox module, then export those names from the sandbox.
  2. Use using in the virtual module to make the sandbox's exported names available.

This allows JET to define names in the virtual module even when the same names already exist in actual, without triggering errors such as cannot assign a value to variable ... from module .... It also allows JET to analyze an existing module other than Main without defining analyzed code directly in that module.

TODO

Because this function relies on Base.names, it cannot reproduce names made available through using.

source
JET.ConcreteInterpreterType
abstract type ConcreteInterpreter <: JuliaInterpreter.Interpreter end

An interface for concretely interpreting selected top-level statements during virtual_process using JuliaInterpreter.

Subtypes must implement:

  • InterpretationState(interp::T) -> InterpretationState: return the interpreter state.
  • ConcreteInterpreter(interp::T, state::InterpretationState) -> T: return an interpreter of type T associated with state.
  • ToplevelAbstractAnalyzer(interp::T) -> ToplevelAbstractAnalyzer: return the top-level analyzer associated with the interpreter.
source
JET.partially_interpret!Function
partially_interpret!(interp::ConcreteInterpreter, concretize::BitVector,
                     mod::Module, src::CodeInfo) -> concretize::BitVector

Resize and fill concretize with one entry for each statement in src; true marks a statement selected for concrete interpretation. Evaluate the selected statements using JuliaInterpreter.jl and return the same selection mask.

The selection includes:

  • Top-level definitions, including :method, :struct_type, :abstract_type, and :primitive_type expressions, together with their dependencies.
  • Module-usage expressions, which are directly evaluated so that invalid usages can be reported. Modules loaded by import or using are not recursively analyzed.
  • include calls, which cause top-level analysis to recursively enter the included file.
source

Analysis result

JET.JETToplevelResultType
res::JETToplevelResult

Represents the result of analyzing top-level code, including files, packages, and text.

  • res.analyzer::AbstractAnalyzer: the AbstractAnalyzer used for the analysis
  • res.res::VirtualProcessResult: the VirtualProcessResult produced by the analysis
  • res.source::AbstractString: a description of the analysis target that also serves as the identity key of the analysis; e.g. the VS Code integration uses it to replace superseded diagnostics
  • res.jetconfigs: the configurations associated with the analysis

JETToplevelResult implements Base.show methods for JET's supported front ends. Julia's display system selects the appropriate method when rendering the analysis result.

source
JET.JETCallResultType
res::JETCallResult

Represents the result of analyzing a function call.

  • res.result::InferenceResult: the InferenceResult produced by the analysis
  • res.analyzer::AbstractAnalyzer: the AbstractAnalyzer used for the analysis
  • res.source::AbstractString: a description of the analysis target that also serves as the identity key of the analysis; e.g. the VS Code integration uses it to replace superseded diagnostics
  • res.jetconfigs: the configurations associated with the analysis

JETCallResult implements Base.show methods for JET's supported front ends. Julia's display system selects the appropriate method when rendering the analysis result.

source

Splitting and filtering reports

Both JETToplevelResult and JETCallResult can be split into individual failures for integration with tools like Cthulhu:

JET.get_reportsFunction
reports = JET.get_reports(result::JETCallResult)
reports = JET.get_reports(result::JETToplevelResult)

Return the reports represented by result, one per detected issue.

For a JETCallResult, this returns the inference reports after applying report configuration. For a JETToplevelResult, top-level errors take precedence: if any top-level errors were collected, only those errors are returned. Otherwise, this returns the configured inference reports.

source
JET.reportkeyFunction
reportkey(report::InferenceErrorReport)

Return (report.sig.tt, last(report.vst).linfo). The first component is the reported call tuple type, or nothing when no call tuple type is available. The second component is the MethodInstance of the final virtual frame.

For a collection of reports, unique(reportkey, reports) deduplicates reports with the same two components, even when they were reached from different analysis entry points.

source

Error report interface

JET.VirtualFrameType
VirtualFrame

Stack information representing a virtual execution context:

  • file::Symbol: the source file containing the execution context
  • line::Int: the source line containing the execution context
  • linfo::MethodInstance: the MethodInstance containing the context

This type is similar to Base.StackTraces.StackFrame, but its context is collected during abstract interpretation rather than runtime execution.

source
JET.VirtualStackTraceType
VirtualStackTrace

A vector of VirtualFrames ordered from the analysis entry point to the error point. The first element represents the entry frame, and the last element represents the frame where the error was detected.

source
JET.SignatureType
Signature

Represents the expression signature associated with an error point.

  • _sig::Vector{Any}: components used to render the expression
  • tt::Union{Type,Nothing}: the call tuple type, when available

Equality compares only the elements of _sig, using ===, and ignores tt. Hashing likewise uses only _sig. print_signature renders a Signature for display.

source
JET.InferenceErrorReportType
abstract type InferenceErrorReport end

An interface type for error reports collected during JET's abstract interpretation.

Every concrete subtype provides the following fields:

A subtype may provide additional fields to explain why the error was reported.

source
JET.ToplevelErrorReportType
abstract type ToplevelErrorReport end

An interface type for reports that JET collects during top-level processing, including parsing and partial concrete interpretation. All concrete subtypes of ToplevelErrorReport must have the following fields:

  • file::String: the path to the source file associated with the report
  • line::Int: the source line associated with the report

See also: virtual_process, ConcreteInterpreter

source