From 0ec6218f152da80007297002cd76ff5fc34251ba Mon Sep 17 00:00:00 2001 From: Tim Holy Date: Sat, 17 Aug 2024 15:15:04 -0500 Subject: [PATCH] Add Cthulhu.ascend integration (#648) Closes #511 Also adds `reportkey` --------- Co-authored-by: Shuhei Kadowaki <40514306+aviatesk@users.noreply.github.com> --- CHANGELOG.md | 6 + Project.toml | 8 +- docs/src/optanalysis.md | 55 ++++++++- ext/JETCthulhuExt.jl | 25 ++++ src/JET.jl | 4 +- src/abstractinterpret/inferenceerrorreport.jl | 114 ++++++++++++------ src/analyzers/jetanalyzer.jl | 8 +- test/ext/test_cthulhu.jl | 12 ++ test/runtests.jl | 4 + 9 files changed, 195 insertions(+), 41 deletions(-) create mode 100644 ext/JETCthulhuExt.jl create mode 100644 test/ext/test_cthulhu.jl diff --git a/CHANGELOG.md b/CHANGELOG.md index be5a8cf60..d85c33735 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +[0.9.8]: https://github.com/aviatesk/JET.jl/compare/v0.9.7...v0.9.8 [0.9.6]: https://github.com/aviatesk/JET.jl/compare/v0.9.5...v0.9.6 [0.9.5]: https://github.com/aviatesk/JET.jl/compare/v0.9.4...v0.9.5 [0.9.4]: https://github.com/aviatesk/JET.jl/compare/v0.9.3...v0.9.4 @@ -25,6 +26,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [0.8.0]: https://github.com/aviatesk/JET.jl/compare/v0.7.15...v0.8.0 +## [0.9.8] +### Added +- An extension that integrates `@report_opt` with Cthulhu (aviatesk/JET.jl#648) +- `reportkey` for trimming multiple reports that resolve to the same runtime-dispatch caller/callee pair (aviatesk/JET.jl#648) + ## [0.9.6] ### Fixed - `report_opt` no longer raises reports from callees on `throw` code path when the diff --git a/Project.toml b/Project.toml index f8659c420..c63aaf468 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "JET" uuid = "c3a54625-cd67-489e-a8e7-0a5a0ff4e31b" authors = ["Shuhei Kadowaki "] -version = "0.9.7" +version = "0.9.8" [deps] CodeTracking = "da1fd8a2-8d9e-5ec2-8556-3022fb5608a2" @@ -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.14.0" Example = "0.5.3" InteractiveUtils = "1.10" JuliaInterpreter = "0.9" @@ -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" @@ -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"] diff --git a/docs/src/optanalysis.md b/docs/src/optanalysis.md index 8bb8beaff..0ea1c2628 100644 --- a/docs/src/optanalysis.md +++ b/docs/src/optanalysis.md @@ -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) @@ -164,6 +164,59 @@ 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) +``` + +!!! tip + If `rpts` is a long list, consider using `urpts = unique(reportkey, rpts)` to trim it. + See [`reportkey`](@ref). + +Now you can `ascend` individual reports: + +``` +julia> using Cthulhu + +julia> ascend(rpts[1]) +Choose a call for analysis (q to quit): + runtime dispatch to make_vals(%1::Any)::Any + > sumup(::typeof(sin)) + +Open an editor at a possible caller of + Tuple{typeof(make_vals), Any} +or browse typed code: + > "REPL[7]", sumup: lines [4] + Browse typed code +``` + +`ascend` will show the full call-chain to reach a particular runtime dispatch; in this case, it was our entry point, but in other cases it may be deeper in the call graph. In this case, we've interactively moved the selector `>` down to the `sumup` call (you cannot descend into the `"runtime dispatch to..."` as there is no known code associated with it) and hit ``, at which point Cthulhu showed us that the call to `make_vals(::Any)` occured only on line 4 of the definition of `sumup` (which we entered at the REPL). Cthulhu is now prompting us to either open the code in an editor (which will fail in this case, since there is no associated file!) or view the type-annoted code. If we select the "Browse typed code" option we see + +``` +sumup(f) @ Main REPL[7]:1 + 1 function sumup(f::Core.Const(sin))::Any + 2 # this function uses the non-constant global variable `n` here + 3 # and it makes every succeeding operations type-unstable + 4 vals::Any = make_vals(n::Any)::Any + 5 s::Any = zero(eltype(vals::Any)::Any)::Any + 6 for v::Any in vals::Any::Any + 7 (s::Any += f::Core.Const(sin)(v::Any)::Any)::Any + 8 end + 9 return s::Any +10 end +Select a call to descend into or ↩ to ascend. [q]uit. [b]ookmark. +⋮ +``` + +with red highlighting to indicate the non-inferable arguments. + +For more information, you're encouraged to read Cthulhu's documentation, which includes a video tutorial better-suited to this interactive tool. ## [Entry Points](@id optanalysis-entry) diff --git a/ext/JETCthulhuExt.jl b/ext/JETCthulhuExt.jl new file mode 100644 index 000000000..3e0cb9929 --- /dev/null +++ b/ext/JETCthulhuExt.jl @@ -0,0 +1,25 @@ +module JETCthulhuExt + +using JET: JET, InferenceErrorReport, VirtualFrame, PrintConfig, print_signature +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) + print_signature(IOContext(io, :color=>true), r.sig, PrintConfig()) + # printstyled(IOContext(io, :color=>true), r.sig.tt, color=:red) + Cthulhu.treelist!(Node(Data{Union{MethodInstance,Type}}("runtime dispatch 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 diff --git a/src/JET.jl b/src/JET.jl index ed2852b49..a97d82274 100644 --- a/src/JET.jl +++ b/src/JET.jl @@ -6,7 +6,7 @@ module JET export # jetanalyzer @report_call, report_call, @test_call, test_call, - report_file, test_file, report_package, test_package, report_text, test_text, + report_file, test_file, report_package, test_package, report_text, reportkey, test_text, watch_file, # optanalyzer @report_opt, report_opt, @test_opt, test_opt, @@ -294,6 +294,8 @@ get_linfo(linfo::MethodInstance) = linfo is_constant_propagated(frame::InferenceState) = is_constant_propagated(frame.result) is_constant_propagated(result::InferenceResult) = CC.any(result.overridden_by_const) +struct TypeUnassigned end # for when inference doesn't bother assigning a type to a slot (e.g. dead code) + # lattice ignorenotfound(@nospecialize(t)) = t === NOT_FOUND ? Bottom : t diff --git a/src/abstractinterpret/inferenceerrorreport.jl b/src/abstractinterpret/inferenceerrorreport.jl index 6b6bd95b8..ecec9e5c0 100644 --- a/src/abstractinterpret/inferenceerrorreport.jl +++ b/src/abstractinterpret/inferenceerrorreport.jl @@ -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 @@ -94,21 +95,18 @@ 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::DataType) get_sig(caller::InferenceResult) = get_sig(get_linfo(caller)) -function get_sig_nowrap(@nospecialize args...) - sig = Any[] - handle_sig!(sig, args...) - return sig -end +const HandleSigRT = Tuple{Vector{Any}, Union{Type,Nothing}} # the return type of `handle_sig!` +get_sig_nowrap(s::StateAtPC, @nospecialize(stmt)) = handle_sig!([], s, stmt)::HandleSigRT 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) @@ -118,29 +116,35 @@ 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 + 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)` @@ -164,12 +168,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 = typeof_arg(s, args[1]) push!(sig, ' ') handle_sig!(sig, s, f) push!(sig, ' ') handle_sig!(sig, s, args[2]) + t2 = typeof_arg(s, args[2]) end push!(sig, safewidenconst(get_ssavaluetype(s))) + push!(sig, Tuple{typeof_arg(s, f), t1, t2}) return true end @@ -184,6 +191,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, Tuple{typeof(getglobal(f.mod, f.name)), typeof_arg(s, args[1]), Symbol}) return true end @@ -202,6 +210,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, Tuple{typeof(getglobal(f.mod, f.name)), typeof_arg(s, args[1]), Symbol, typeof_arg(s, args[3])}) return true end @@ -217,6 +226,7 @@ function handle_sig_getindex!(sig::Vector{Any}, s::StateAtPC, f::GlobalRef, args end push!(sig, ']') push!(sig, safewidenconst(get_ssavaluetype(s))) + push!(sig, Tuple{typeof(getglobal(f.mod, f.name)), [typeof_arg(s, arg) for arg in args]...}) return true end @@ -236,6 +246,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, Tuple{typeof(getglobal(f.mod, f.name)), [typeof_arg(s, arg) for arg in args]...}) return true end @@ -244,6 +255,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, Tuple{typeof(getglobal(f.mod, f.name)), [typeof_arg(s, arg) for arg in args]...}) return true end @@ -258,25 +270,28 @@ function handle_sig_call!(sig::Vector{Any}, s::StateAtPC, @nospecialize(f), args splat::Bool = false) handle_sig!(sig, s, f) push!(sig, '(') + typs = Any[typeof_arg(s, f; callable=true)] nargs = length(args) for (i, arg) in enumerate(args) + push!(typs, typeof_arg(s, arg)) handle_sig!(sig, s, arg) 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) @@ -292,8 +307,7 @@ function handle_sig_assignment!(sig::Vector{Any}, s::StateAtPC, expr::Expr) end end end - handle_sig!(sig, s, last(expr.args)) - return sig + return handle_sig!(sig, s, last(expr.args))::HandleSigRT end function handle_sig_static_parameter!(sig::Vector{Any}, s::StateAtPC, expr::Expr) @@ -302,7 +316,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) @@ -325,7 +339,7 @@ function handle_sig!(sig::Vector{Any}, (sv, _)::StateAtPC, ssa::SSAValue) # XXX the same problem may happen for `InferenceState` too ? handle_sig!(sig, newstate, get_stmt(newstate)) end - return sig + return sig, nothing end function handle_sig!(sig::Vector{Any}, s::StateAtPC, slot::SlotNumber) @@ -334,7 +348,7 @@ function handle_sig!(sig::Vector{Any}, s::StateAtPC, slot::SlotNumber) if istoplevel(sv) # this is a abstract global variable, form the global reference handle_sig!(sig, s, GlobalRef(sv.linfo.def::Module, name)) - return sig + return sig, nothing end if name === Symbol("") repr = slot # fallback if no explicit slotname @@ -345,7 +359,7 @@ function handle_sig!(sig::Vector{Any}, s::StateAtPC, slot::SlotNumber) typ = safewidenconst((sv isa InferenceState && CC.is_inferred(sv)) ? get_slottype(sv, slot) : get_slottype(s, slot)) push!(sig, repr, typ) - return sig + return sig, nothing end # NOTE `Argument` is introduced by optimization, and so we don't need to handle abstract global variable here @@ -358,13 +372,13 @@ function handle_sig!(sig::Vector{Any}, (sv, _)::StateAtPC, arg::Argument) end typ = safewidenconst(get_slottype(sv, arg)) # after optimization we shouldn't use `get_slottype(::StateAtPC, ::Any)` push!(sig, repr, typ) - return sig + return sig, nothing end function handle_sig!(sig::Vector{Any}, s::StateAtPC, gotoifnot::GotoIfNot) push!(sig, "goto ", SSAValue(gotoifnot.dest), " if not ") handle_sig!(sig, s, gotoifnot.cond) - return sig + return sig, nothing end function handle_sig!(sig::Vector{Any}, s::StateAtPC, rn::ReturnNode) @@ -374,7 +388,7 @@ function handle_sig!(sig::Vector{Any}, s::StateAtPC, rn::ReturnNode) push!(sig, "return ") handle_sig!(sig, s, rn.val) end - return sig + return sig, nothing end is_unreachable(@nospecialize(x)) = isa(x, ReturnNode) && !isdefined(x, :val) @@ -382,19 +396,41 @@ function handle_sig!(sig::Vector{Any}, ::StateAtPC, qn::QuoteNode) v = qn.value if isa(v, Symbol) push!(sig, Repr(v)) - return sig + return sig, nothing end typ = typeof(v) push!(sig, qn, typ) - return sig + return sig, nothing end # reprs -handle_sig!(sig::Vector{Any}, ::StateAtPC, x::Symbol) = (push!(sig, Repr(x)); return sig) -handle_sig!(sig::Vector{Any}, ::StateAtPC, x::String) = (push!(sig, Repr(x)); return sig) +handle_sig!(sig::Vector{Any}, ::StateAtPC, x::Symbol) = (push!(sig, Repr(x)); return sig, nothing) +handle_sig!(sig::Vector{Any}, ::StateAtPC, x::String) = (push!(sig, Repr(x)); return sig, nothing) # fallback: GlobalRef, literals... -handle_sig!(sig::Vector{Any}, ::StateAtPC, @nospecialize(x)) = (push!(sig, x); return sig) +handle_sig!(sig::Vector{Any}, ::StateAtPC, @nospecialize(x)) = (push!(sig, x); return sig, nothing) + +function typeof_arg(s::State, @nospecialize(f); callable::Bool=false) + isa(f, GlobalRef) && return isdefined(f.mod, f.name) ? Core.Typeof(getglobal(f.mod, f.name)) : Any + isa(f, SSAValue) && return safewidenconst(get_ssavaluetype((s, f.id))) + isa(f, Function) && return Core.Typeof(f) + isa(f, Type) && return Type{f} + isa(f, QuoteNode) && return Core.Typeof(f.value) + isexpr(f, :static_parameter) && return Core.Typeof(s.sptypes[first(f.args)::Int]) + callable && error("f ", string(f)::String, " with type ", string(typeof(f)), " not supported") # FIXME self check runtime dispatch + return typeof(f) +end +function typeof_arg(s::StateAtPC, @nospecialize(f); kwargs...) + if isa(f, SlotNumber) + ret = safewidenconst(get_slottype(s, f)) + ret === Union{} || return ret + # "broken" calls end up here, e.g., one where an argument (not necessarily this one) is undefined and inference doesn't bother assigning types + # for the other args + return TypeUnassigned # One can't create Tuple{typeof(f), Union{}} so we use a placeholder + end + isa(f, Core.Argument) && return safewidenconst(get_slottype(s, f)) + return typeof_arg(first(s), f; kwargs...) +end # new report # ---------- @@ -544,6 +580,16 @@ end # utility # ------- +""" + reportkey(report::InferenceErrorReport) + +Returns an identifier for the runtime-dispatched call site of `report`. + +If you have a long list of reports to analyze, `urpts = unique(reportkey, rpts)` may remove "duplicates" +that arrive at the same runtime dispatch from different entry points. +""" +reportkey(report::InferenceErrorReport) = (report.sig.tt, report.vst[end].linfo) + # TODO parametric definition? """ diff --git a/src/analyzers/jetanalyzer.jl b/src/analyzers/jetanalyzer.jl index 5ec61d116..a92991b16 100644 --- a/src/analyzers/jetanalyzer.jl +++ b/src/analyzers/jetanalyzer.jl @@ -476,13 +476,15 @@ function UncaughtExceptionReport(sv::InferenceState, throw_calls::Vector{Tuple{I vf = get_virtual_frame(sv.linfo) vst = VirtualFrame[vf] sigs = Any[] + tt = Union{} ncalls = length(throw_calls) for (i, (pc, call)) in enumerate(throw_calls) - call_sig = get_sig_nowrap((sv, pc), call) + call_sig, call_tt = get_sig_nowrap((sv, pc), call) append!(sigs, call_sig) + tt = Union{tt, call_tt} i ≠ ncalls && push!(sigs, ", ") end - sig = Signature(sigs) + sig = Signature(sigs, tt) single_error = ncalls == 1 return UncaughtExceptionReport(vst, sig, single_error) end @@ -626,7 +628,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`: diff --git a/test/ext/test_cthulhu.jl b/test/ext/test_cthulhu.jl new file mode 100644 index 000000000..9d6462dc1 --- /dev/null +++ b/test/ext/test_cthulhu.jl @@ -0,0 +1,12 @@ +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 DataType + @test only(parent.children).data.nd isa Core.MethodInstance +end diff --git a/test/runtests.jl b/test/runtests.jl index 03174418c..c077c4a6d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -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