Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Cthulhu.ascend integration #648

Merged
merged 16 commits into from
Aug 17, 2024
6 changes: 5 additions & 1 deletion Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,18 @@ Preferences = "21216c6a-2e73-6563-6e65-726566657250"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[weakdeps]
Cthulhu = "f68482b8-f384-11e8-15f7-abe071a5a75f"
Revise = "295af30f-e4ad-537b-8983-00126c2a3abe"

[extensions]
JETCthulhuExt = "Cthulhu"
ReviseExt = "Revise"

[compat]
Aqua = "0.8.2"
BenchmarkTools = "1.3.2"
CodeTracking = "1.3.1"
Cthulhu = "2.12.7"
Example = "0.5.3"
InteractiveUtils = "1.10"
JuliaInterpreter = "0.9"
Expand All @@ -43,6 +46,7 @@ julia = "1.10"
[extras]
Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595"
BenchmarkTools = "6e4b80f9-dd63-53aa-95a3-0cdb28fa8baf"
Cthulhu = "f68482b8-f384-11e8-15f7-abe071a5a75f"
Example = "7876af07-990d-54b4-ab0e-23690620f79a"
Libdl = "8f399da3-3557-5675-b5ff-fb832c97cbdb"
Logging = "56ddb016-857b-54e1-b83d-db4d58db5568"
Expand All @@ -52,4 +56,4 @@ StaticArrays = "90137ffa-7385-5640-81b9-e52037218182"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Aqua", "BenchmarkTools", "Example", "Libdl", "Logging", "Random", "Revise", "StaticArrays", "Test"]
test = ["Aqua", "BenchmarkTools", "Cthulhu", "Example", "Libdl", "Logging", "Random", "Revise", "StaticArrays", "Test"]
26 changes: 25 additions & 1 deletion docs/src/optanalysis.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ JET implements such an analyzer that investigates the optimized representation o
anywhere the compiler failed in optimization. Especially, it can find where Julia creates captured variables, where
runtime dispatch will happen, and where Julia gives up the optimization work due to unresolvable recursive function call.

[SnoopCompile also detects inference failures](https://timholy.github.io/SnoopCompile.jl/stable/snoopi_deep_analysis/), but JET and SnoopCompile use different mechanisms: JET performs *static* analysis of a particular call, while SnoopCompile performs *dynamic* analysis of new inference. As a consequence, JET's detection of inference failures is reproducible (you can run the same analysis repeatedly and get the same result) but terminates at any non-inferable node of the call graph: you will miss runtime dispatch in any non-inferable callees. Conversely, SnoopCompile's detection of inference failures can explore the entire callgraph, but only for those portions that have not been previously inferred, and the analysis cannot be repeated in the same session.
[SnoopCompile also detects inference failures](https://timholy.github.io/SnoopCompile.jl/stable/tutorials/snoop_inference_analysis/), but JET and SnoopCompile use different mechanisms: JET performs *static* analysis of a particular call, while SnoopCompile performs *dynamic* analysis of new inference. As a consequence, JET's detection of inference failures is reproducible (you can run the same analysis repeatedly and get the same result) but terminates at any non-inferable node of the call graph: you will miss runtime dispatch in any non-inferable callees. Conversely, SnoopCompile's detection of inference failures can explore the entire callgraph, but only for those portions that have not been previously inferred, and the analysis cannot be repeated in the same session.

## [Quick Start](@id optanalysis-quick-start)

Expand Down Expand Up @@ -164,6 +164,30 @@ using Test
end
```

## [Integration with Cthulhu](@id cthulhu-integration)

If you identify inference problems, you may want to fix them. Cthulhu can be a useful tool for gaining more insight, and JET integrates nicely with Cthulhu.

To exploit Cthulhu, you first need to split the overall report into individual inference failures:

```@repl quickstart
report = @report_opt sumup(sin);
rpts = JET.get_reports(report)
```

Now you can `ascend` individual reports:

```
julia> using Cthulhu

julia> ascend(rpts[1])
Choose a call for analysis (q to quit):
> sumup(::typeof(sin))
```

`ascend` will show the full call-chain to reach a particlar runtime dispatch; in this case, it was our entry point, but in other cases it may be deeper in the call graph.

Because Cthulhu is an interactive terminal program, we can't demonstrate it in this page, but you're encouraged to see Cthulhu's documentation which includes a video tutorial.

## [Entry Points](@id optanalysis-entry)

Expand Down
24 changes: 24 additions & 0 deletions ext/JETCthulhuExt.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module JETCthulhuExt

using JET: JET, InferenceErrorReport, VirtualFrame
using Cthulhu: Cthulhu, Node, Data, callstring
using Core: MethodInstance

const _emptybackedges = MethodInstance[]

struct CallFrames
frames::Vector{VirtualFrame}
end

function Cthulhu.treelist(r::InferenceErrorReport)
io = IOBuffer()
cf = CallFrames(r.vst)
printstyled(IOContext(io, :color=>true), r.sig.tt, color=:red)
Cthulhu.treelist!(Node(Data{Union{MethodInstance,Type}}("runtime call to " * String(take!(io)), r.sig.tt)), io, cf, "", Base.IdSet{Union{MethodInstance,Nothing}}([nothing]))
end

Cthulhu.instance(cf::CallFrames) = isempty(cf.frames) ? nothing : cf.frames[end].linfo
Cthulhu.backedges(cf::CallFrames) = isempty(cf.frames) ? _emptybackedges : [cf.frames[end].linfo]
Cthulhu.nextnode(cf::CallFrames, ::MethodInstance) = CallFrames(cf.frames[1:end-1])

end
67 changes: 48 additions & 19 deletions src/abstractinterpret/inferenceerrorreport.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ Represents an expression signature.
"""
struct Signature
_sig::Vector{Any}
tt::Union{Type,Nothing}
end

# define equality functions that avoid dynamic dispatches
Expand Down Expand Up @@ -94,21 +95,21 @@ end
# signature
# ---------

@inline get_sig(s::StateAtPC, @nospecialize(x=get_stmt(s))) = Signature(get_sig_nowrap(s, x))
@inline get_sig(s::StateAtPC, @nospecialize(x=get_stmt(s))) = Signature(get_sig_nowrap(s, x)...)
get_sig(sv::InferenceState) = get_sig((sv, get_currpc(sv)))

get_sig(mi::MethodInstance) = Signature(Any[mi])
get_sig(mi::MethodInstance) = Signature(Any[mi], mi.specTypes)
get_sig(caller::InferenceResult) = get_sig(get_linfo(caller))

function get_sig_nowrap(@nospecialize args...)
sig = Any[]
handle_sig!(sig, args...)
return sig
sig, tt = handle_sig!(sig, args...)
timholy marked this conversation as resolved.
Show resolved Hide resolved
return sig, tt
end

function handle_sig!(sig::Vector{Any}, s::StateAtPC, expr::Expr)
head = expr.head
if head === :call
sig, tt = if head === :call
handle_sig_call!(sig, s, expr)
elseif head === :invoke
handle_sig_invoke!(sig, s, expr)
Expand All @@ -118,29 +119,36 @@ function handle_sig!(sig::Vector{Any}, s::StateAtPC, expr::Expr)
handle_sig_static_parameter!(sig, s, expr)
else
push!(sig, expr)
sig, nothing
end
return sig
@show tt head s[2]
timholy marked this conversation as resolved.
Show resolved Hide resolved
return sig, tt
end

function handle_sig_call!(sig::Vector{Any}, s::StateAtPC, expr::Expr)
function splitlast!(list)
last = pop!(list)
return list, last
end

f = first(expr.args)
args = expr.args[2:end]
splat = false
if isa(f, GlobalRef)
handle_sig_binop!(sig, s, f, args) && return sig
handle_sig_getproperty!(sig, s, f, args) && return sig
handle_sig_setproperty!!(sig, s, f, args) && return sig
handle_sig_getindex!(sig, s, f, args) && return sig
handle_sig_setindex!!(sig, s, f, args) && return sig
handle_sig_const_apply_type!(sig, s, f, args) && return sig
handle_sig_binop!(sig, s, f, args) && return splitlast!(sig)
handle_sig_getproperty!(sig, s, f, args) && return splitlast!(sig)
handle_sig_setproperty!!(sig, s, f, args) && return splitlast!(sig)
handle_sig_getindex!(sig, s, f, args) && return splitlast!(sig)
handle_sig_setindex!!(sig, s, f, args) && return splitlast!(sig)
handle_sig_const_apply_type!(sig, s, f, args) && return splitlast!(sig)
if issplat(f, args)
f = args[2]
args = args[3:end]
splat = true
end
end
handle_sig_call!(sig, s, f, args, #=splat=#splat)
return sig
sig, tt = handle_sig_call!(sig, s, f, args, #=splat=#splat)
return sig, tt
end

# create a type-annotated signature for `([sig of ex]::T)`
Expand All @@ -164,12 +172,15 @@ function handle_sig_binop!(sig::Vector{Any}, s::StateAtPC, f::GlobalRef, args::V
(Base.isbinaryoperator(f.name) && length(args) == 2) || return false
@annotate_if_active sig begin
handle_sig!(sig, s, args[1])
t1 = sig[end]
push!(sig, ' ')
handle_sig!(sig, s, f)
push!(sig, ' ')
handle_sig!(sig, s, args[2])
t2 = sig[end]
end
push!(sig, safewidenconst(get_ssavaluetype(s)))
push!(sig, Tuple{typeof_sig_f(s, f), t1, t2})
return true
end

Expand All @@ -184,6 +195,7 @@ function handle_sig_getproperty!(sig::Vector{Any}, s::StateAtPC, f::GlobalRef, a
push!(sig, '.')
push!(sig, String(val))
push!(sig, safewidenconst(get_ssavaluetype(s)))
push!(sig, nothing) # FIXME
return true
end

Expand All @@ -202,6 +214,7 @@ function handle_sig_setproperty!!(sig::Vector{Any}, s::StateAtPC, f::GlobalRef,
handle_sig!(sig, s, args[3])
push!(sig, safewidenconst(get_ssavaluetype(s)))
end
push!(sig, nothing) # FIXME
return true
end

Expand All @@ -217,6 +230,7 @@ function handle_sig_getindex!(sig::Vector{Any}, s::StateAtPC, f::GlobalRef, args
end
push!(sig, ']')
push!(sig, safewidenconst(get_ssavaluetype(s)))
push!(sig, nothing) # FIXME
return true
end

Expand All @@ -236,6 +250,7 @@ function handle_sig_setindex!!(sig::Vector{Any}, s::StateAtPC, f::GlobalRef, arg
handle_sig!(sig, s, args[2])
push!(sig, safewidenconst(get_ssavaluetype(s)))
end
push!(sig, nothing) # FIXME
return true
end

Expand All @@ -244,6 +259,7 @@ function handle_sig_const_apply_type!(sig::Vector{Any}, s::StateAtPC, f::GlobalR
typ = get_ssavaluetype(s)
isa(typ, Const) || return false
push!(sig, ApplyTypeResult(typ.val))
push!(sig, nothing) # FIXME
return true
end

Expand All @@ -258,25 +274,30 @@ function handle_sig_call!(sig::Vector{Any}, s::StateAtPC, @nospecialize(f), args
splat::Bool = false)
handle_sig!(sig, s, f)
push!(sig, '(')
@show f typeof(f)
typs = Any[typeof_sig_f(s, f)]
@show typs
nargs = length(args)
for (i, arg) in enumerate(args)
handle_sig!(sig, s, arg)
push!(typs, sig[end])
timholy marked this conversation as resolved.
Show resolved Hide resolved
if i ≠ nargs
push!(sig, ", ")
else
splat && push!(sig, "...")
end

end
push!(sig, ')')
push!(sig, safewidenconst(get_ssavaluetype(s)))
return sig
return sig, Tuple{typs...}
end

function handle_sig_invoke!(sig::Vector{Any}, s::StateAtPC, expr::Expr)
f = expr.args[2]
args = expr.args[3:end]
handle_sig_call!(sig, s, f, args)
return sig
sig, tt = handle_sig_call!(sig, s, f, args)
return sig, tt
end

function handle_sig_assignment!(sig::Vector{Any}, s::StateAtPC, expr::Expr)
Expand All @@ -293,7 +314,7 @@ function handle_sig_assignment!(sig::Vector{Any}, s::StateAtPC, expr::Expr)
end
end
handle_sig!(sig, s, last(expr.args))
return sig
return sig, nothing
end

function handle_sig_static_parameter!(sig::Vector{Any}, s::StateAtPC, expr::Expr)
Expand All @@ -302,7 +323,7 @@ function handle_sig_static_parameter!(sig::Vector{Any}, s::StateAtPC, expr::Expr
name = sparam_name((sv.linfo.def::Method).sig::UnionAll, i)
typ = widenconst(sv.sptypes[i].typ)
push!(sig, String(name), typ)
return sig
return sig, nothing
end

function sparam_name(u::UnionAll, i::Int)
Expand Down Expand Up @@ -396,6 +417,14 @@ handle_sig!(sig::Vector{Any}, ::StateAtPC, x::String) = (push!(sig, Repr(x)); re
# fallback: GlobalRef, literals...
handle_sig!(sig::Vector{Any}, ::StateAtPC, @nospecialize(x)) = (push!(sig, x); return sig)

function typeof_sig_f(s::State, @nospecialize(f))
isa(f, GlobalRef) ? Core.Typeof(getglobal(f.mod, f.name)) :
isa(f, Core.Argument) ? safewidenconst(s.slottypes[f.n]) :
isa(f, SSAValue ? safewidenconst(s.ssavaluetypes[f.id]) :
error("f ", f, " with type ", typeof(f), " not supported"))
end
typeof_sig_f(s::StateAtPC, @nospecialize(f)) = typeof_sig_f(first(s), f)

# new report
# ----------

Expand Down
4 changes: 2 additions & 2 deletions src/analyzers/jetanalyzer.jl
Original file line number Diff line number Diff line change
Expand Up @@ -482,7 +482,7 @@ function UncaughtExceptionReport(sv::InferenceState, throw_calls::Vector{Tuple{I
append!(sigs, call_sig)
i ≠ ncalls && push!(sigs, ", ")
end
sig = Signature(sigs)
sig = Signature(sigs, nothing)
single_error = ncalls == 1
return UncaughtExceptionReport(vst, sig, single_error)
end
Expand Down Expand Up @@ -626,7 +626,7 @@ end

const REDUCE_EMPTY_REPORT_SIG = let
sig = Any["MethodError: reducing over an empty collection is not allowed; consider supplying `init` to the reducer"]
Signature(sig)
Signature(sig, nothing)
end

# special case `reduce_empty` and `mapreduce_empty`:
Expand Down
11 changes: 11 additions & 0 deletions test/ext/test_cthulhu.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using JET, Cthulhu

@testset begin
getsomething(x::Array) = x[]
computesomething(x) = getsomething(x) + 1

rpt = @report_opt computesomething(Any[1])
r = only(JET.get_reports(rpt))
parent = Cthulhu.treelist(r)
@test parent.data.nd isa Core.MethodInstance
end
4 changes: 4 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,8 @@ using Test, JET
@testset "sanity check" include("sanity_check.jl")

@testset "self check" include("self_check.jl")

@testset "extensions" begin
include("ext/test_cthulhu.jl")
end
end
Loading