Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
7 changes: 6 additions & 1 deletion src/Cthulhu.jl
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ const IRShow = Base.IRShow
using Accessors
using CodeTracking: CodeTracking
using InteractiveUtils
using InteractiveUtils: is_expected_union
# `is_expected_union` moved from InteractiveUtils into `Compiler.IRShow` on Julia 1.14.
@static if isdefined(Base.Compiler.IRShow, :is_expected_union)
using Base.Compiler.IRShow: is_expected_union
else
using InteractiveUtils: is_expected_union
end
using UUIDs
using REPL: REPL, AbstractTerminal
using JuliaSyntax: JuliaSyntax, children
Expand Down
17 changes: 14 additions & 3 deletions src/CthulhuCompiler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,24 @@ using .Cthulhu: CthulhuState, CthulhuConfig, CallInfo, Callsite, cached_exceptio
using Base: isvarargtype, unwrapva, unwrap_unionall, mapany, get_world_counter
using JuliaSyntax: JuliaSyntax, children, is_leaf

using .CC: AbstractInterpreter, CallMeta, ApplyCallInfo, CallInfo as CCCallInfo, ConstCallInfo,
using .CC: AbstractInterpreter, CallMeta, ApplyCallInfo, CallInfo as CCCallInfo,
EFFECTS_TOTAL, Effects, IncrementalCompact, InferenceParams, InferenceResult,
InferenceState, IRCode, LimitedAccuracy, MethodMatchInfo, MethodResultPure,
NativeInterpreter, NoCallInfo, OptimizationParams, OptimizationState,
UnionSplitApplyCallInfo, UnionSplitInfo, WorldRange, WorldView,
UnionSplitApplyCallInfo, UnionSplitInfo, WorldRange,
argextype, argtypes_to_type, compileable_specialization, ignorelimited, singleton_type,
specialize_method, sptypes_from_meth_instance, widenconst, method_table, findsup,
cached_return_type

# `ConstCallInfo` was removed on Julia 1.14 (#59413): const-prop results now live directly
# in `MethodMatchInfo.call_results`. Use a placeholder so `isa` checks compile on both.
@static if isdefined(CC, :ConstCallInfo)
const ConstCallInfoT = CC.ConstCallInfo
else
struct _NoConstCallInfo end
const ConstCallInfoT = _NoConstCallInfo
end

const ArgTypes = Vector{Any}

include("compiler/callsite.jl")
Expand All @@ -29,6 +38,8 @@ get_effects(codeinst::CodeInstance) = CC.decode_effects(codeinst.ipo_purity_bits
get_effects(codeinst::CodeInfo) = CC.decode_effects(codeinst.purity)
get_effects(result::InferenceResult) = result.ipo_effects
get_effects(source::InferredSource) = source.effects
get_effects(result::CC.ConstPropResult) = get_effects(result.result)
@static if isdefined(CC, :ConstPropResult)
get_effects(result::CC.ConstPropResult) = get_effects(result.result)
end
get_effects(result::CC.ConcreteResult) = result.effects
get_effects(result::CC.SemiConcreteResult) = result.effects
17 changes: 12 additions & 5 deletions src/compiler/codeview.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,18 @@ function cthulhu_llvm(io::IO, provider::AbstractProvider, state::CthulhuState, r
(; config) = state
(; optimize, debuginfo) = config
world = get_inference_world(provider)
dump = InteractiveUtils._dump_function_llvm(
state.mi, result.src,
#=wrapper=# false, !raw,
dump_module, optimize, debuginfo !== :none ? :source : :none,
Base.CodegenParams())
di = debuginfo !== :none ? :source : :none
# `_dump_function_llvm` gained an `llvm_options::String` argument on Julia 1.14.
@static if hasmethod(InteractiveUtils._dump_function_llvm,
Tuple{Core.MethodInstance,Core.CodeInfo,Bool,Bool,Bool,Bool,Symbol,String,Base.CodegenParams})
dump = InteractiveUtils._dump_function_llvm(
state.mi, result.src, #=wrapper=# false, !raw,
dump_module, optimize, di, #=llvm_options=# "", Base.CodegenParams())
else
dump = InteractiveUtils._dump_function_llvm(
state.mi, result.src, #=wrapper=# false, !raw,
dump_module, optimize, di, Base.CodegenParams())
end
highlight(io, dump, "llvm", config)
end

Expand Down
4 changes: 3 additions & 1 deletion src/compiler/interface.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ show_parameters(io::IO, provider::AbstractProvider, interp::AbstractInterpreter)
function show_inference_cache(io::IO, interp::AbstractInterpreter)
@info "Dumping inference cache."
cache = CC.get_inference_cache(interp)
for (i, (; linfo, result)) in enumerate(cache)
# `get_inference_cache` returns a `Compiler.InferenceCache` on Julia 1.14+, a plain vector before.
results = cache isa AbstractVector ? cache : cache.results
for (i, (; linfo, result)) in enumerate(results)
println(io, i, ": ", linfo, "::", result)
end
end
Expand Down
18 changes: 18 additions & 0 deletions src/compiler/interpreter.jl
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,23 @@ CC.retrieve_ir_for_inlining(cached_result::CodeInstance, src::OptimizedSource) =
CC.retrieve_ir_for_inlining(mi::MethodInstance, src::OptimizedSource, preserve_local_sources::Bool) =
CC.retrieve_ir_for_inlining(mi, src.ir, preserve_local_sources)

# Since #59413 (Julia 1.14) the source is obtained via `ci_get_source` and passed as the
# 5th argument to `IRInterpretationState` (replacing the old `world::UInt`); `va_process_argtypes`
# also gained a trailing `mi`. Without these overrides the generic constructor rejects Cthulhu's
# `OptimizedSource` (it isn't a `CodeInfo`) and semi-concrete eval silently degrades to const-prop.
@static if isdefined(CC, :ci_get_source)
CC.ci_get_source(::CthulhuInterpreter, code::CodeInstance) = code.inferred
function CC.IRInterpretationState(interp::CthulhuInterpreter,
code::CodeInstance, mi::MethodInstance, argtypes::Vector{Any}, @nospecialize(inferred))
isa(inferred, OptimizedSource) || return nothing
ir = CC.copy(inferred.ir)
src = inferred.src
spec_info = CC.SpecInfo(src)
argtypes = CC.va_process_argtypes(CC.optimizer_lattice(interp), argtypes, src.nargs, src.isva, mi)
return CC.IRInterpretationState(interp, spec_info, ir, mi, argtypes,
code.min_world, code.max_world)
end
else
function CC.IRInterpretationState(interp::CthulhuInterpreter,
code::CodeInstance, mi::MethodInstance, argtypes::Vector{Any}, world::UInt)
inferred = code.inferred
Expand All @@ -243,3 +260,4 @@ function CC.IRInterpretationState(interp::CthulhuInterpreter,
return CC.IRInterpretationState(interp, spec_info, ir, mi, argtypes, world,
code.min_world, code.max_world)
end
end
89 changes: 67 additions & 22 deletions src/compiler/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,25 @@ function transform(::Val{:CuFunction}, provider, callsite, callexpr, src, mi, sl
return Callsite(callsite.id, CuCallInfo(callinfo(provider, sig, Nothing; world)), callsite.head)
end

# Returns the closure argument of a `Core._Task(closure, ...)` construction, else `nothing`.
# Task creation shows up as an `:invoke`/`:call` to `Core._Task` when the constructor isn't
# inlined to a `jl_new_task` foreigncall (the latter is handled separately in `find_callsites`).
function task_constructor_closure(@nospecialize(stmt), src, sptypes, slottypes)
isa(stmt, Expr) || return nothing
stmt = ignorelhs(stmt)
isa(stmt, Expr) || return nothing
if stmt.head === :invoke
length(stmt.args) ≥ 3 || return nothing
CC.singleton_type(argextype(stmt.args[2], src, sptypes, slottypes)) === Core._Task || return nothing
return stmt.args[3]
elseif stmt.head === :call
length(stmt.args) ≥ 2 || return nothing
CC.singleton_type(argextype(stmt.args[1], src, sptypes, slottypes)) === Core._Task || return nothing
return stmt.args[2]
end
return nothing
end

function find_callsites(provider::AbstractProvider, result::LookupResult, ci::CodeInstance, annotate_source::Bool=false, pc2excts::Union{Nothing,PC2Excts}=nothing)
mi = get_mi(ci)
sptypes = sptypes_from_meth_instance(mi)
Expand All @@ -28,7 +47,13 @@ function find_callsites(provider::AbstractProvider, result::LookupResult, ci::Co
stmt = stmts[id]
isa(stmt, Expr) || continue
callsite = nothing
if is_call_expr(stmt, result.optimized)
local taskclosure = task_constructor_closure(stmt, src, sptypes, result.slottypes)
if taskclosure !== nothing
ftype = widenconst(argextype(taskclosure, src, sptypes, result.slottypes))
sig = Tuple{ftype}
callsite = Callsite(id, TaskCallInfo(callinfo(provider, sig, nothing; world=get_inference_world(provider))), (ignorelhs(stmt)::Expr).head)
end
if callsite === nothing && is_call_expr(stmt, result.optimized)
info = result.infos[id]
if info !== nothing
if isa(info, CC.UnionSplitApplyCallInfo)
Expand Down Expand Up @@ -127,30 +152,36 @@ function process_const_info(provider::AbstractProvider, ::LookupResult, @nospeci
@nospecialize(exct))
if isnothing(result)
return thisinfo
elseif result isa CC.VolatileInferenceResult
# NOTE we would not hit this case since `finish!(::CthulhuInterpreter, frame::InferenceState)`
# will always transform `frame.result.src` to `OptimizedSource` when frame is inferred
return thisinfo
elseif isa(result, CC.ConcreteResult)
edge = result.edge
effects = get_effects(result)
mici = EdgeCallInfo(edge, rt, effects, exct)
return ConcreteCallInfo(mici, argtypes)
elseif isa(result, CC.ConstPropResult)
effects = get_effects(result)
result = result.result
mici = EdgeCallInfo(result.ci_as_edge, rt, effects, exct)
return ConstPropCallInfo(mici, result)
elseif isa(result, CC.SemiConcreteResult)
effects = get_effects(result)
mici = EdgeCallInfo(result.edge, rt, effects, exct)
return SemiConcreteCallInfo(mici, result.ir)
end
@static if isdefined(CC, :ConstPropResult)
if isa(result, CC.VolatileInferenceResult)
# NOTE we would not hit this case since `finish!(::CthulhuInterpreter, frame::InferenceState)`
# will always transform `frame.result.src` to `OptimizedSource` when frame is inferred
return thisinfo
elseif isa(result, CC.ConstPropResult)
result = result.result
end
else
@assert isa(result, CC.InferenceResult)
effects = get_effects(result)
mici = EdgeCallInfo(result.ci_as_edge, rt, effects, exct)
return ConstPropCallInfo(mici, result)
# Since #59413 `MethodMatchInfo.call_results` also carries the regular (non-const)
# edge inference result; only genuine constant-prop results override argtypes.
if isa(result, CC.InferenceResult)
obc = result.overridden_by_const
(obc === nothing || !any(obc)) && return thisinfo
end
end
@assert isa(result, CC.InferenceResult)
effects = get_effects(result)
mici = EdgeCallInfo(result.ci_as_edge, rt, effects, exct)
return ConstPropCallInfo(mici, result)
end

function process_info(provider::AbstractProvider, result::LookupResult, @nospecialize(info::CCCallInfo),
Expand All @@ -172,14 +203,28 @@ function process_info(provider::AbstractProvider, result::LookupResult, @nospeci
end
end
if isa(info, MethodMatchInfo)
return CallInfo[let
if edge === nothing
RTCallInfo(unwrapconst(argtypes[1]), argtypes[2:end], rt, exct)
else
effects = @something(effects, get_effects(edge))
EdgeCallInfo(edge, rt, effects, exct)
@static if isdefined(CC, :ConstCallInfo)
return CallInfo[let
if edge === nothing
RTCallInfo(unwrapconst(argtypes[1]), argtypes[2:end], rt, exct)
else
effects = @something(effects, get_effects(edge))
EdgeCallInfo(edge, rt, effects, exct)
end
end for edge in info.edges if edge !== nothing]
else
# Since #59413 const-prop results are attached per-edge in `info.call_results`.
infos = CallInfo[]
for i in eachindex(info.edges)
edge = info.edges[i]
edge === nothing && continue
eff = @something(effects, get_effects(edge))
thisinfo = EdgeCallInfo(edge, rt, eff, exct)
thisinfo = process_const_info(provider, result, thisinfo, argtypes, rt, info.call_results[i], exct)
push!(infos, thisinfo)
end
end for edge in info.edges if edge !== nothing]
return infos
end
elseif isa(info, UnionSplitInfo)
return mapreduce(process_recursive, vcat, info.split; init=CallInfo[])::Vector{CallInfo}
elseif isa(info, UnionSplitApplyCallInfo)
Expand All @@ -188,7 +233,7 @@ function process_info(provider::AbstractProvider, result::LookupResult, @nospeci
# XXX: This could probably use its own info. For now,
# we ignore any implicit iterate calls.
return process_recursive(info.call)
elseif isa(info, ConstCallInfo)
elseif isa(info, ConstCallInfoT)
infos = process_recursive(info.call)
@assert length(infos) == length(info.results)
return CallInfo[let
Expand Down
16 changes: 14 additions & 2 deletions test/providers/OverlayProviderModule.jl
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,20 @@ end

mutable struct OverlayToken end

# The local inference cache type changed from `Vector{InferenceResult}` to
# `Compiler.InferenceCache` on Julia 1.14 (#59413).
@static if isdefined(CC, :InferenceCache)
const LocalCache = CC.InferenceCache
new_local_cache() = CC.InferenceCache()
else
const LocalCache = Vector{InferenceResult}
new_local_cache() = InferenceResult[]
end

struct OverlayInterpreter <: AbstractInterpreter
token::OverlayToken
method_table::OverlayMethodTable
local_cache::Vector{InferenceResult}
local_cache::LocalCache
world::UInt
inference_parameters::InferenceParams
optimization_parameters::OptimizationParams
Expand All @@ -35,7 +45,7 @@ function OverlayInterpreter(world::UInt = Base.get_world_counter();
return OverlayInterpreter(
OverlayToken(),
OverlayMethodTable(world, METHOD_TABLE),
InferenceResult[],
new_local_cache(),
world,
inference_parameters,
optimization_parameters,
Expand Down Expand Up @@ -80,5 +90,7 @@ CompilerIntegration.OptimizedSource(provider::OverlayProvider, interp::OverlayIn
OptimizedSource(provider, provider.cthulhu, result)
CompilerIntegration.InferredSource(provider::OverlayProvider, interp::OverlayInterpreter, ci::CodeInstance) =
InferredSource(provider, provider.cthulhu, ci)
CompilerIntegration.InferredSource(provider::OverlayProvider, interp::OverlayInterpreter, result::InferenceResult) =
InferredSource(provider, provider.cthulhu, result)

end
8 changes: 6 additions & 2 deletions test/test_AbstractInterpreter.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ macro newinterp(InterpName, ephemeral_cache::Bool=false)
InterpName = esc(InterpName)
C = Core
CC = Cthulhu.CC
# The local inference cache type changed from `Vector{InferenceResult}` to
# `Compiler.InferenceCache` on Julia 1.14 (#59413).
InfCacheType = isdefined(CC, :InferenceCache) ? :($CC.InferenceCache) : :(Vector{$CC.InferenceResult})
InfCacheInit = isdefined(CC, :InferenceCache) ? :($CC.InferenceCache()) : :($CC.InferenceResult[])
quote
$(ephemeral_cache && quote
struct $InterpCacheName
Expand All @@ -37,13 +41,13 @@ macro newinterp(InterpName, ephemeral_cache::Bool=false)
world::UInt
inf_params::$CC.InferenceParams
opt_params::$CC.OptimizationParams
inf_cache::Vector{$CC.InferenceResult}
inf_cache::$InfCacheType
$(ephemeral_cache && :(code_cache::$InterpCacheName))
function $InterpName(meta = nothing;
world::UInt = Base.get_world_counter(),
inf_params::$CC.InferenceParams = $CC.InferenceParams(),
opt_params::$CC.OptimizationParams = $CC.OptimizationParams(),
inf_cache::Vector{$CC.InferenceResult} = $CC.InferenceResult[],
inf_cache::$InfCacheType = $InfCacheInit,
$(ephemeral_cache ?
Expr(:kw, :(code_cache::$InterpCacheName), :($InterpCacheName())) :
Expr(:kw, :_, :nothing)))
Expand Down
28 changes: 19 additions & 9 deletions test/test_Cthulhu.jl
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,9 @@ end
callsite.head === :invoke || return false
io = IOBuffer()
print(io, callsite)
return occursin("invoke throw_boundserror", String(take!(io)))
# Base renamed `throw_boundserror` to `_throw_boundserror_indices` on Julia 1.14;
# match the shared substring (the `:invoke` head is already asserted above).
return occursin("throw_boundserror", String(take!(io)))
end

callsites = find_callsites_by_ftt(calltwice, Tuple{Vector{AbstractFloat}})
Expand All @@ -117,7 +119,9 @@ end
callsite.head === :invoke || return false
io = IOBuffer()
print(io, callsite)
return occursin("invoke throw_boundserror", String(take!(io)))
# Base renamed `throw_boundserror` to `_throw_boundserror_indices` on Julia 1.14;
# match the shared substring (the `:invoke` head is already asserted above).
return occursin("throw_boundserror", String(take!(io)))
end

# Note the failure of `callinfo` to properly handle specialization
Expand Down Expand Up @@ -290,12 +294,13 @@ end
# constant prop' on all the splits
callsites = find_callsites_by_ftt(x -> semi_concrete_eval(42, x), (Int,); optimize = false)
callinfo = only(callsites).info
@test isa(callinfo, Cthulhu.SemiConcreteCallInfo)
# On Julia 1.14 the compiler const-props this through instead of semi-concrete eval'ing it.
@test isa(callinfo, Cthulhu.SemiConcreteCallInfo) || isa(callinfo, Cthulhu.ConstPropCallInfo)
@test Cthulhu.get_rt(callinfo) == Const(semi_concrete_eval(42, 0))
# @test Cthulhu.get_effects(callinfo) |> CC.is_semiconcrete_eligible
io = IOBuffer()
print(io, only(callsites))
@test occursin("= < semi-concrete eval > semi_concrete_eval(::Const(42),::$Int)", String(take!(io)))
@test occursin("semi_concrete_eval(::Const(42),::$Int)", String(take!(io)))
end

function bar346(x::ComplexF64)
Expand All @@ -304,8 +309,9 @@ function bar346(x::ComplexF64)
end
@testset "issue #346" begin
callsites = find_callsites_by_ftt(bar346, Tuple{ComplexF64}; optimize=false)
@test isa(callsites[1].info, Cthulhu.SemiConcreteCallInfo)
@test occursin("= < semi-concrete eval > getproperty(::ComplexF64,::Const(:re))::Float64", string(callsites[1]))
# On Julia 1.14 the compiler const-props this through instead of semi-concrete eval'ing it.
@test isa(callsites[1].info, Cthulhu.SemiConcreteCallInfo) || isa(callsites[1].info, Cthulhu.ConstPropCallInfo)
@test occursin("getproperty(::ComplexF64,::Const(:re))::Float64", string(callsites[1]))
@test Cthulhu.get_rt(callsites[end].info) == Const(sin(1.0))
end

Expand Down Expand Up @@ -453,7 +459,8 @@ invoke_constcall(a::Number, c::Bool) = c ? Number : :number
@test Cthulhu.get_rt(info) === rt
buf = IOBuffer()
show(buf, callsite)
@test isa(inner, Cthulhu.SemiConcreteCallInfo)
# On Julia 1.14 the compiler const-props this through instead of semi-concrete eval'ing it.
@test isa(inner, Cthulhu.SemiConcreteCallInfo) || isa(inner, Cthulhu.ConstPropCallInfo)
@test occursin("= invoke < invoke_constcall(::Any,::$(Const(true)))::$rt", String(take!(buf)))
end

Expand Down Expand Up @@ -628,7 +635,9 @@ end
end
@test occursin("invoke f1()::…\n", doprint(m.f1))
str = doprint(m.f2)
@test occursin("y::Const([1, 2, 3", str)
# On Julia 1.14 the slot type widens to `Vector{Int64}`; the `Const` array shows on the
# assignment RHS instead of the slot. Either way the constant must be displayed (and truncated).
@test occursin("Const([1, 2, 3", str)
@test !occursin("500,", str)
end

Expand Down Expand Up @@ -706,7 +715,8 @@ end
micallee_Int = find_method_instance(provider, callee, (Int,))
micallee_Float64 = find_method_instance(provider, callee, (Float64,))
info, lines = only(Cthulhu.find_caller_of(provider, micallee_Int, micaller))
@test info == (:caller, Symbol(@__FILE__), 0) && lines == [line1, line3]
# Callsite discovery order isn't guaranteed (it flipped on Julia 1.14); compare order-independently.
@test info == (:caller, Symbol(@__FILE__), 0) && sort(lines) == sort([line1, line3])
info, lines = only(Cthulhu.find_caller_of(provider, micallee_Float64, micaller))
@test info == (:caller, Symbol(@__FILE__), 0) && lines == [line2]

Expand Down
Loading
Loading