Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
9dd5755
compat: add Julia 1.13 to supported versions
Mar 30, 2026
bafca2f
print: guard CodeTracking.whereis for Base/Core submodules
Mar 30, 2026
209105d
virtualprocess: handle Julia 1.13 binding partition IR
Mar 30, 2026
d88d529
typeinfer: handle declare_const in abstract evaluation
Mar 30, 2026
1878c72
jetanalyzer: filter binding partition builtins in sound mode
Mar 30, 2026
78f02ea
test: update test expectations for Julia 1.13
Mar 30, 2026
c165f4c
Fix some world age issues
Apr 1, 2026
55118d2
Add compatibility to LoweredCodeUtils v3.6
Jun 15, 2026
a4e2306
Only do type inference filtering when having an `InferenceState`
Jun 17, 2026
0e89c30
compat: gate WorldView and ConstCallInfo imports for the 1.14 Compiler
koalazub Jun 27, 2026
7a1f41c
compat: resolve the code-cache view to InternalCodeCache on the 1.14 …
koalazub Jun 27, 2026
7feda3f
compat: dispatch constprop_cache_lookup on the 1.14 Compiler
koalazub Jun 27, 2026
a0db904
compat: skip cache_result! where the 1.14 Compiler removed it
koalazub Jun 27, 2026
7209225
compat: unwrap ConstCallInfo only when the type still exists
koalazub Jun 27, 2026
5368623
compat: gate InferenceState.world access for the Julia 1.14 dev Compiler
koalazub Jun 27, 2026
2191ba1
compat: widen cache_target to parametric InferenceState
koalazub Jun 28, 2026
ebdb1c4
compat: port the analyzer to the current Julia 1.14 dev Compiler
koalazub Jul 1, 2026
dba10ba
toplevel: record and materialize non-const global assignment types
koalazub Jul 6, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,4 @@ PrecompileTools = "1.3.2"
Preferences = "1.4"
Revise = "3.13"
Test = "1.10"
julia = "1.12"
julia = "1.12, 1.13"
3 changes: 2 additions & 1 deletion src/JET.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ using .Preferences: UUID

const JET_DEV_MODE = Preferences.load_preference(UUID("c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"), "JET_DEV_MODE", false)

const USE_FIXED_WORLD = Preferences.load_preference(UUID("c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"), "use_fixed_world", !JET_DEV_MODE)
const USE_FIXED_WORLD = Preferences.load_preference(UUID("c3a54625-cd67-489e-a8e7-0a5a0ff4e31b"), "use_fixed_world",
VERSION < v"1.13.0-" ? !JET_DEV_MODE : false)

const PKG_EVAL = Base.get_bool_env("JULIA_PKGEVAL", false)

Expand Down
17 changes: 14 additions & 3 deletions src/JETBase.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,21 @@ using Core: Builtin, IntrinsicFunction, Intrinsics, SimpleVector, svec
using Core.IR

using .CC: @nospecs, AbstractInterpreter, AbstractLattice, ArgInfo, Bottom,
CachedMethodTable, CallMeta, ConstCallInfo, EFFECTS_THROWS, Future, InferenceParams,
CachedMethodTable, CallMeta, EFFECTS_THROWS, Future, InferenceParams,
InferenceResult, InferenceState, InvokeCallInfo, MethodCallResult, MethodMatchInfo,
NOT_FOUND, OptimizationParams, OptimizationState, OverlayMethodTable, RTEffects,
StatementState, StmtInfo, UnionSplitInfo, VarState, VarTable, WorldRange, WorldView,
StatementState, StmtInfo, UnionSplitInfo, VarState, VarTable, WorldRange,
argextype, argtype_by_index, argtypes_to_type, hasintersect, ignorelimited,
instanceof_tfunc, singleton_type, slot_id, specialize_method, tmeet, tmerge,
typeinf_lattice, widenconst, widenlattice, ⊑

@static if isdefined(CC, :ConstCallInfo)
using .CC: ConstCallInfo
end
@static if isdefined(CC, :WorldView)
using .CC: WorldView
end

using Base: PkgId, generating_output, get_world_counter

using Base.Meta: isexpr, lower
Expand Down Expand Up @@ -252,7 +259,11 @@ end
stmt_types(sv::InferenceState) = StmtTypes(sv)
function Base.getindex(st::StmtTypes, pc::Int)
block = CC.block_for_inst(st.sv.cfg, pc)
return st.sv.bb_vartables[block]::VarTable
@static if isdefined(CC, :BBEntryState)
return (st.sv.bb_states[block]::CC.BBEntryState).vartable::VarTable
else
return st.sv.bb_vartables[block]::VarTable
end
end

function is_compileable_mi(mi::MethodInstance)
Expand Down
20 changes: 14 additions & 6 deletions src/abstractinterpret/abstractanalyzer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ struct AbstractBindingState
end
const AbstractBindings = IdDict{Core.BindingPartition,AbstractBindingState}

@static if isdefined(CC, :InferenceCache)
const JETLocalCache = CC.InferenceCache
new_local_cache() = CC.InferenceCache()
else
const JETLocalCache = Vector{InferenceResult}
new_local_cache() = InferenceResult[]
end

"""
mutable struct AnalyzerState
...
Expand Down Expand Up @@ -116,7 +124,7 @@ mutable struct AnalyzerState
## AbstractInterpreter ##

const world::UInt
const inf_cache::Vector{InferenceResult}
const inf_cache::JETLocalCache
const inf_params::InferenceParams
const opt_params::OptimizationParams

Expand All @@ -130,7 +138,7 @@ mutable struct AnalyzerState

# the temporal stash to keep track of the context of caller inference/optimization and
# the caller itself, to which reconstructed cached reports will be appended
cache_target::Union{Nothing,Pair{Symbol,InferenceState}}
cache_target::Union{Nothing,Pair{Symbol,<:InferenceState}}

## abstract toplevel execution ##

Expand All @@ -148,7 +156,7 @@ for fld in fieldnames(AnalyzerState)
getter = Symbol("get_", fld)
@eval (@__MODULE__) @inline $getter(analyzer::AbstractAnalyzer) = getfield(AnalyzerState(analyzer), $(QuoteNode(fld)))
end
set_cache_target!(analyzer::AbstractAnalyzer, target::Union{Nothing,Pair{Symbol,InferenceState}}) = setfield!(AnalyzerState(analyzer), :cache_target, target)
set_cache_target!(analyzer::AbstractAnalyzer, target::Union{Nothing,Pair{Symbol,<:InferenceState}}) = setfield!(AnalyzerState(analyzer), :cache_target, target)
set_entry!(analyzer::AbstractAnalyzer, entry::Union{Nothing,MethodInstance}) = setfield!(AnalyzerState(analyzer), :entry, entry)

# The main constructor used at analysis entries
Expand All @@ -157,12 +165,12 @@ function AnalyzerState(world::UInt = get_world_counter();
inf_params = JETInferenceParams(; jetconfigs...)
opt_params = JETOptimizationParams(; jetconfigs...)
return AnalyzerState(#=world::UInt=# world,
#=inf_cache::Vector{InferenceResult}=# InferenceResult[],
#=inf_cache::JETLocalCache=# new_local_cache(),
#=inf_params::InferenceParams=# inf_params,
#=opt_params::OptimizationParams=# opt_params,
#=analysis_results::IdDict{InferenceResult,AnalysisResult}=# IdDict{InferenceResult,AnalysisResult}(),
#=report_stash::Vector{InferenceErrorReport}=# InferenceErrorReport[],
#=cache_target::Union{Nothing,Pair{Symbol,InferenceState}}=# nothing,
#=cache_target::Union{Nothing,Pair{Symbol,<:InferenceState}}=# nothing,
#=concretized::BitVector=# non_toplevel_concretized,
#=binding_states::AbstractBindings=# AbstractBindings(),
#=entry::Union{Nothing,MethodInstance}=# nothing)
Expand All @@ -177,7 +185,7 @@ function AnalyzerState(state::AnalyzerState, refresh_local_cache::Bool=true;
binding_states::AbstractBindings = state.binding_states,
entry::Union{Nothing,MethodInstance} = state.entry)
if refresh_local_cache
inf_cache = InferenceResult[]
inf_cache = new_local_cache()
analysis_results = IdDict{InferenceResult,AnalysisResult}()
else
(; inf_cache, analysis_results) = state
Expand Down
158 changes: 134 additions & 24 deletions src/abstractinterpret/typeinfer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,15 @@ end
# global
# ------

@static if !isdefined(CC, :WorldView)
struct WorldView{Cache}
cache::Cache
worlds::WorldRange
WorldView(cache::Cache, worlds::WorldRange) where Cache = new{Cache}(cache, worlds)
end
WorldView(cache, args...) = WorldView(cache, WorldRange(args...))
end

CC.cache_owner(analyzer::AbstractAnalyzer) = AnalysisToken(analyzer)

function CC.code_cache(analyzer::AbstractAnalyzer)
Expand All @@ -236,8 +245,16 @@ function CC.code_cache(analyzer::AbstractAnalyzer)
return WorldView(view, worlds)
end

to_internal_code_cache_view(wvc::WorldView{<:AbstractAnalyzerView}) =
WorldView(CC.InternalCodeCache(CC.cache_owner(wvc.cache.analyzer)), wvc.worlds)
@static if !isdefined(CC, :WorldView)
CC.code_cache(analyzer::AbstractAnalyzer, worlds::WorldRange) =
WorldView(AbstractAnalyzerView(analyzer), worlds)

to_internal_code_cache_view(wvc::WorldView{<:AbstractAnalyzerView}) =
CC.InternalCodeCache(CC.cache_owner(wvc.cache.analyzer), wvc.worlds)
else
to_internal_code_cache_view(wvc::WorldView{<:AbstractAnalyzerView}) =
WorldView(CC.InternalCodeCache(CC.cache_owner(wvc.cache.analyzer)), wvc.worlds)
end

CC.haskey(wvc::WorldView{<:AbstractAnalyzerView}, mi::MethodInstance) = haskey(to_internal_code_cache_view(wvc), mi)

Expand Down Expand Up @@ -287,24 +304,35 @@ function CC.getindex(wvc::WorldView{<:AbstractAnalyzerView}, mi::MethodInstance)
return codeinst::CodeInstance
end

function CC.setindex!(wvc::WorldView{<:AbstractAnalyzerView}, codeinst::CodeInstance, mi::MethodInstance)
return to_internal_code_cache_view(wvc)[mi] = codeinst
@static if isdefined(CC, :cache_result!)
function CC.setindex!(wvc::WorldView{<:AbstractAnalyzerView}, codeinst::CodeInstance, mi::MethodInstance)
return to_internal_code_cache_view(wvc)[mi] = codeinst
end
else
function CC.setindex!(wvc::WorldView{<:AbstractAnalyzerView}, codeinst::CodeInstance, mi::MethodInstance)
istoplevelframe(mi) && return codeinst
return to_internal_code_cache_view(wvc)[mi] = codeinst
end
end

# local
# -----

CC.get_inference_cache(analyzer::AbstractAnalyzer) = AbstractAnalyzerView(analyzer)

function CC.cache_lookup(𝕃ᵢ::CC.AbstractLattice, mi::MethodInstance, given_argtypes::Argtypes, view::AbstractAnalyzerView)
function jet_constprop_cache_lookup(𝕃ᵢ::CC.AbstractLattice, mi::MethodInstance, given_argtypes::Argtypes, view::AbstractAnalyzerView)
# XXX the very dirty analyzer state observation again
# this method should only be called from the single context i.e. `abstract_call_method_with_const_args`,
# and so we should reset the cache target immediately we reach here
analyzer = view.analyzer
cache_target = get_cache_target(analyzer)
set_cache_target!(analyzer, nothing)

inf_result = CC.cache_lookup(𝕃ᵢ, mi, given_argtypes, get_inf_cache(view.analyzer))
inf_result = @static if isdefined(CC, :constprop_cache_lookup)
CC.constprop_cache_lookup(𝕃ᵢ, mi, given_argtypes, get_inf_cache(view.analyzer))
else
CC.cache_lookup(𝕃ᵢ, mi, given_argtypes, get_inf_cache(view.analyzer))
end

isa(inf_result, InferenceResult) || return inf_result

Expand Down Expand Up @@ -332,6 +360,14 @@ function CC.cache_lookup(𝕃ᵢ::CC.AbstractLattice, mi::MethodInstance, given_
return inf_result
end

@static if isdefined(CC, :constprop_cache_lookup)
CC.constprop_cache_lookup(𝕃ᵢ::CC.AbstractLattice, mi::MethodInstance, given_argtypes::Argtypes, view::AbstractAnalyzerView) =
jet_constprop_cache_lookup(𝕃ᵢ, mi, given_argtypes, view)
else
CC.cache_lookup(𝕃ᵢ::CC.AbstractLattice, mi::MethodInstance, given_argtypes::Argtypes, view::AbstractAnalyzerView) =
jet_constprop_cache_lookup(𝕃ᵢ, mi, given_argtypes, view)
end

CC.push!(view::AbstractAnalyzerView, inf_result::InferenceResult) = CC.push!(get_inf_cache(view.analyzer), inf_result)

# main driver
Expand All @@ -341,8 +377,11 @@ CC.push!(view::AbstractAnalyzerView, inf_result::InferenceResult) = CC.push!(get
function CC.typeinf(analyzer::AbstractAnalyzer, frame::InferenceState)
parent = CC.frame_parent(frame)

if is_constant_propagated(frame) && parent !== nothing
parent::InferenceState
# The parent of a constant-propagated frame is an `InferenceState` when const-prop' is
# entered from regular abstract interpretation, but an `IRInterpretationState` when it is
# entered from semi-concrete interpretation (IR interpretation). Only the former carries
# the `InferenceState`-based report stash that the lineage filtering operates on.
if is_constant_propagated(frame) && parent isa InferenceState
# JET is going to perform the abstract-interpretation with the extended lattice elements:
# throw-away the error reports that are collected during the previous non-constant abstract-interpretation
# NOTE that the `linfo` here is the exactly same object as the method instance used
Expand Down Expand Up @@ -490,33 +529,92 @@ function CC.global_assignment_rt_exct(analyzer::ToplevelAbstractAnalyzer, sv::In
isconditional = istoplevelframe(sv) ? let postdomtree = CC.construct_postdomtree(sv.cfg)
!CC.postdominates(postdomtree, sv.currbb, 1)
end : true
(valid_worlds, ret) = CC.scan_partitions(analyzer, g, sv.world) do analyzer::AbstractAnalyzer, ::Core.Binding, partition::Core.BindingPartition
@static if hasfield(InferenceState, :world)
worldhint = sv.world
else
curworld = CC.get_inference_world(analyzer)
worldhint = CC.binding_world_hints(curworld, sv)
end
(valid_worlds, ret) = CC.scan_partitions(analyzer, g, worldhint) do analyzer::AbstractAnalyzer, ::Core.Binding, partition::Core.BindingPartition
rte = CC.global_assignment_binding_rt_exct(analyzer, partition, newty′[])
if isconcretized
# skip the assignment effect if this has been concretized already
else
# Non-const bindings may be assigned in any call, so it is fundamentally impossible
# to track their types precisely.
# However, by accurately determining whether a top-level assignment is conditional,
# it is possible to track such bindings’ `isdefined` status precisely.
get_binding_states(analyzer)[partition] = AbstractBindingState(false, isconditional)
# Record the widened join of observed assignment types (best-effort for
# script analysis — non-const bindings are reassignable from any call).
binding_states = get_binding_states(analyzer)
prev = get(binding_states, partition, nothing)
maybeundef = isconditional && (prev === nothing || prev.maybeundef)
assigned = newty′[]
binding_state = if assigned === Union{}
AbstractBindingState(false, maybeundef)
else
typ = CC.widenconst(assigned)
if prev !== nothing && isdefined(prev, :typ)
typ = CC.tmerge(CC.typeinf_lattice(analyzer), prev.typ, typ)
end
AbstractBindingState(false, maybeundef, typ)
end
binding_states[partition] = binding_state
# HACK/FIXME Concretize `AbstractBindingState` (same convention as the
# `:const` path above) so later analyses resolve the binding via the module
# namespace. Guarded so a value set by `ConcreteInterpreter` is never clobbered.
world = Base.get_world_counter()
if !Base.invoke_in_world(world, isdefinedglobal, g.mod, g.name) ||
Base.invoke_in_world(world, getglobal, g.mod, g.name) isa AbstractBindingState
Core.eval(g.mod, Expr(:block, Expr(:global, g.name),
Expr(:(=), g.name, QuoteNode(binding_state))))
end
end
return rte
end
CC.update_valid_age!(sv, valid_worlds)
@static if hasfield(InferenceState, :world)
CC.update_valid_age!(sv, valid_worlds)
else
CC.update_valid_age!(sv, curworld, valid_worlds)
end
return ret
end

function CC.abstract_eval_statement_expr(analyzer::ToplevelAbstractAnalyzer, e::Expr, sstate::StatementState,
sv::InferenceState)::Future{RTEffects}
if isexpr(e, :const)
if !isconcretized(analyzer, sv) # skip the assignment effect if this has been concretized already
return abstract_eval_const_stmt(analyzer, e, sstate, sv)
end
if !isconcretized(analyzer, sv)
isexpr(e, :const) && return abstract_eval_const_stmt(analyzer, e, sstate, sv)
is_declare_const_call(e) && return abstract_eval_declare_const(analyzer, e, sstate, sv)
end
return @invoke CC.abstract_eval_statement_expr(analyzer::AbstractAnalyzer, e::Expr, sstate::StatementState, sv::InferenceState)
end

function is_declare_const_call(e::Expr)::Bool
isexpr(e, :call) || return false
length(e.args) ≥ 3 || return false
f = e.args[1]
return f isa GlobalRef && f.mod === Core && f.name === :declare_const
end

# Handle `Core.declare_const(mod, :name, value)` calls (Julia 1.13+)
# by converting them to the equivalent abstract handling of `:const` expressions
function abstract_eval_declare_const(analyzer::ToplevelAbstractAnalyzer, stmt::Expr,
sstate::StatementState, sv::InferenceState)
istoplevelframe(sv) || return RTEffects(Union{}, ErrorException, EFFECTS_THROWS)
mod_arg = stmt.args[2]
name_arg = stmt.args[3]
name_arg isa QuoteNode || return RTEffects(Nothing, ErrorException, EFFECTS_THROWS)
name = name_arg.value::Symbol
mod = mod_arg isa Module ? mod_arg : CC.frame_module(sv)
gr = GlobalRef(mod, name)
if length(stmt.args) == 4
lastargtype = CC.abstract_eval_value(analyzer, stmt.args[4], sstate, sv)
if CC.isvarargtype(lastargtype)
return RTEffects(Nothing, ErrorException, EFFECTS_THROWS)
end
else
lastargtype = nothing
end
rt, exct = const_assignment_rt_exct(analyzer, sv, sstate.saw_latestworld, gr, lastargtype)
return RTEffects(rt, exct, CC.Effects(EFFECTS_THROWS; nothrow=exct===Union{}))
end

# XXX Do we need to port this back to Julia base?
function abstract_eval_const_stmt(analyzer::ToplevelAbstractAnalyzer, stmt::Expr, sstate::StatementState, sv::InferenceState)
na = length(stmt.args)
Expand Down Expand Up @@ -552,7 +650,13 @@ function const_assignment_rt_exct(analyzer::ToplevelAbstractAnalyzer, sv::Infere
new_binding_typ′ = Ref{Any}(new_binding_typ)
postdomtree = CC.construct_postdomtree(sv.cfg)
isconditional = !CC.postdominates(postdomtree, sv.currbb, 1)
(valid_worlds, ret) = CC.scan_partitions(analyzer, gr, sv.world) do analyzer::ToplevelAbstractAnalyzer, _binding::Core.Binding, partition::Core.BindingPartition
@static if hasfield(InferenceState, :world)
worldhint = sv.world
else
curworld = CC.get_inference_world(analyzer)
worldhint = CC.binding_world_hints(curworld, sv)
end
(valid_worlds, ret) = CC.scan_partitions(analyzer, gr, worldhint) do analyzer::ToplevelAbstractAnalyzer, _binding::Core.Binding, partition::Core.BindingPartition
rte = const_assignment_binding_rt_exct(analyzer, partition)
rt, _exct = rte
if rt !== Union{}
Expand All @@ -579,7 +683,11 @@ function const_assignment_rt_exct(analyzer::ToplevelAbstractAnalyzer, sv::Infere
end
return rte
end
CC.update_valid_age!(sv, valid_worlds)
@static if hasfield(InferenceState, :world)
CC.update_valid_age!(sv, valid_worlds)
else
CC.update_valid_age!(sv, curworld, valid_worlds)
end
return ret
end

Expand Down Expand Up @@ -644,7 +752,9 @@ end

is_inactive_exception(@nospecialize rt) = isa(rt, Const) && rt.val === _INACTIVE_EXCEPTION()

function CC.cache_result!(analyzer::ToplevelAbstractAnalyzer, caller::InferenceResult, ci::CodeInstance)
istoplevelframe(caller.linfo) && return nothing # don't need to cache toplevel frame
@invoke CC.cache_result!(analyzer::AbstractAnalyzer, caller::InferenceResult, ci::CodeInstance)
@static if isdefined(CC, :cache_result!)
function CC.cache_result!(analyzer::ToplevelAbstractAnalyzer, caller::InferenceResult, ci::CodeInstance)
istoplevelframe(caller.linfo) && return nothing # don't need to cache toplevel frame
@invoke CC.cache_result!(analyzer::AbstractAnalyzer, caller::InferenceResult, ci::CodeInstance)
end
end
Loading