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.AnalysisResult — Type
AnalysisResultContainer 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).
JET.CachedAnalysisResult — Type
CachedAnalysisResultA 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.
JET.AnalysisToken — Type
mutable struct AnalysisToken
AnalysisToken() = new()
endAn 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.
Top-level analysis
JET.virtual_process — Function
virtual_process(interp::ConcreteInterpreter,
x::Union{AbstractString,JS.SyntaxNode},
filename::AbstractString,
config::ToplevelConfig;
overrideex::Union{Nothing,Expr}=nothing) -> res::VirtualProcessResultSimulates 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:
- If
blkis or expands to a:moduleexpression, define the module and recursively analyze its body in that new module. - Otherwise, expand macros and lower
blk. Skip literal results; a:thunkresult supplies the loweredCodeInfo. - If the context module was virtualized, rewrite self-references from the original module to the virtual module; see
fix_self_references!. - Use
ConcreteInterpreterto concretely interpret statements that must not be abstracted, such as:methoddefinitions; seepartially_interpret!. - Use
ToplevelAbstractAnalyzerto analyze the remaining statements abstractly.
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.
JET.VirtualProcessResult — Type
res::VirtualProcessResultres.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 overinference_error_reports.res.inference_error_reports::Vector{InferenceErrorReport}: reports of potential errors found byToplevelAbstractAnalyzer.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 isnothingwhen module virtualization is disabled.
JET.virtualize_module_context — Function
virtualize_module_context(actual::Module)Return a fresh virtual module that provides access to the bindings of actual.
Virtualization proceeds in two steps:
- Use
usingto make the defined names ofactualavailable in a sandbox module, then export those names from the sandbox. - Use
usingin 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.
JET.ConcreteInterpreter — Type
abstract type ConcreteInterpreter <: JuliaInterpreter.Interpreter endAn 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 typeTassociated withstate.ToplevelAbstractAnalyzer(interp::T) -> ToplevelAbstractAnalyzer: return the top-level analyzer associated with the interpreter.
JET.partially_interpret! — Function
partially_interpret!(interp::ConcreteInterpreter, concretize::BitVector,
mod::Module, src::CodeInfo) -> concretize::BitVectorResize 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_typeexpressions, together with their dependencies. - Module-usage expressions, which are directly evaluated so that invalid usages can be reported. Modules loaded by
importorusingare not recursively analyzed. includecalls, which cause top-level analysis to recursively enter the included file.
Analysis result
JET.JETToplevelResult — Type
res::JETToplevelResultRepresents the result of analyzing top-level code, including files, packages, and text.
res.analyzer::AbstractAnalyzer: theAbstractAnalyzerused for the analysisres.res::VirtualProcessResult: theVirtualProcessResultproduced by the analysisres.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 diagnosticsres.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.
JET.JETCallResult — Type
res::JETCallResultRepresents the result of analyzing a function call.
res.result::InferenceResult: theInferenceResultproduced by the analysisres.analyzer::AbstractAnalyzer: theAbstractAnalyzerused for the analysisres.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 diagnosticsres.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.
Splitting and filtering reports
Both JETToplevelResult and JETCallResult can be split into individual failures for integration with tools like Cthulhu:
JET.get_reports — Function
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.
JET.reportkey — Function
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.
Error report interface
JET.VirtualFrame — Type
VirtualFrameStack information representing a virtual execution context:
file::Symbol: the source file containing the execution contextline::Int: the source line containing the execution contextlinfo::MethodInstance: theMethodInstancecontaining the context
This type is similar to Base.StackTraces.StackFrame, but its context is collected during abstract interpretation rather than runtime execution.
JET.VirtualStackTrace — Type
VirtualStackTraceA 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.
JET.Signature — Type
SignatureRepresents the expression signature associated with an error point.
_sig::Vector{Any}: components used to render the expressiontt::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.
JET.InferenceErrorReport — Type
abstract type InferenceErrorReport endAn interface type for error reports collected during JET's abstract interpretation.
Every concrete subtype provides the following 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 subtype may provide additional fields to explain why the error was reported.
JET.ToplevelErrorReport — Type
abstract type ToplevelErrorReport endAn 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 reportline::Int: the source line associated with the report
See also: virtual_process, ConcreteInterpreter