Repository: JuliaDebug/Cthulhu.jl
Pull request #522 (2023-12-20 11:29:51)

implement integration for exception type inference
Integration for JuliaLang/julia#51754.
Statement-wise and call-wise information is available only after `v"1.11.0-DEV.1127"`.
![Screenshot 2023-12-20 at 7 57 01 PM](https://github.com/JuliaDebug/Cthulhu.jl/assets/40514306/05290aa5-49a4-413a-92cc-90f4b49a1b5f)


codecov-commenter: ## [Codecov](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
Attention: `117 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`82f678d`)](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/commit/82f678d639d8a9e9278419c7401dac8b6edfa1a2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 0.00% compared to head [(`c166295`)](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 0.00%.

| [Files](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Patch % | Lines |
|---|---|---|
| [src/callsite.jl](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NhbGxzaXRlLmps) | 0.00% | [36 Missing :warning: ](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [src/Cthulhu.jl](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL0N0aHVsaHUuamw=) | 0.00% | [35 Missing :warning: ](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [src/reflection.jl](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3JlZmxlY3Rpb24uamw=) | 0.00% | [13 Missing :warning: ](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [src/codeview.jl](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NvZGV2aWV3Lmps) | 0.00% | [12 Missing :warning: ](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [src/ui.jl](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL3VpLmps) | 0.00% | [8 Missing :warning: ](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [src/interpreter.jl](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2ludGVycHJldGVyLmps) | 0.00% | [7 Missing :warning: ](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |
| [src/interface.jl](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2ludGVyZmFjZS5qbA==) | 0.00% | [6 Missing :warning: ](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |

<details><summary>Additional details and impacted files</summary>


```diff
@@          Coverage Diff           @@
##           master    #522   +/-   ##
======================================
  Coverage    0.00%   0.00%           
======================================
  Files           9       9           
  Lines        1497    1553   +56     
======================================
- Misses       1497    1553   +56     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/522?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/src/Cthulhu.jl b/src/Cthulhu.jl
index 295216a..a61d6ef 100644
--- a/src/Cthulhu.jl
+++ b/src/Cthulhu.jl
@@ -33,6 +33,7 @@ Base.@kwdef mutable struct CthulhuConfig
     hide_type_stable::Bool = false
     remarks::Bool = false
     with_effects::Bool = false
+    exception_type::Bool = false
     inline_cost::Bool = false
     type_annotations::Bool = true
     annotate_source::Bool = true   # overrides optimize, although the current setting is preserved
@@ -63,6 +64,7 @@ end
 - `iswarn::Bool`: Initial state of "warn" toggle. Defaults to `false`.
 - `remarks::Bool` Initial state of "remarks" toggle. Defaults to `false`.
 - `with_effects::Bool` Intial state of "effects" toggle. Defaults to `false`.
+- `exception_type::Bool` `Intial state of "exception type" toggle. Defaults to `false`.
 - `inline_cost::Bool` Initial state of "inlining costs" toggle. Defaults to `false`.
 - `type_annotations::Bool` Initial state of "type annnotations" toggle. Defaults to `true`.
 - `annotate_source::Bool` Initial state of "Source". Defaults to `true`.
@@ -253,6 +255,14 @@ function cached_return_type(code::CodeInstance)
 end
 end
 
+function cached_exception_type(code::CodeInstance)
+    @static if VERSION ≥ v"1.11.0-DEV.945"
+        return code.exctype
+    else
+        return nothing
+    end
+end
+
 get_effects(codeinst::CodeInstance) = CC.decode_effects(codeinst.ipo_purity_bits)
 get_effects(codeinst::CodeInfo) = CC.decode_effects(codeinst.purity)
 get_effects(src::InferredSource) = src.effects
@@ -275,6 +285,7 @@ end
 function lookup_optimized(interp::CthulhuInterpreter, mi::MethodInstance, allow_no_src::Bool=false)
     codeinst = interp.opt[mi]
     rt = cached_return_type(codeinst)
+    exct = cached_exception_type(codeinst)
     opt = codeinst.inferred
     if opt !== nothing
         opt = opt::OptimizedSource
@@ -296,19 +307,19 @@ function lookup_optimized(interp::CthulhuInterpreter, mi::MethodInstance, allow_
         error("couldn't find the source; inspect `Main.interp` and `Main.mi`")
     end
     effects = get_effects(codeinst)
-    return (; src, rt, infos, slottypes, effects, codeinf)
+    return (; src, rt, exct, infos, slottypes, effects, codeinf)
 end
 
 function lookup_unoptimized(interp::CthulhuInterpreter, mi::MethodInstance)
     codeinf = src = copy(interp.unopt[mi].src)
-    rt = interp.unopt[mi].rt
+    (; rt, exct) = interp.unopt[mi]
     infos = interp.unopt[mi].stmt_info
     effects = get_effects(interp.unopt[mi])
     slottypes = src.slottypes
     if isnothing(slottypes)
         slottypes = Any[ Any for i = 1:length(src.slotflags) ]
     end
-    return (; src, rt, infos, slottypes, effects, codeinf)
+    return (; src, rt, exct, infos, slottypes, effects, codeinf)
 end
 
 function lookup_constproped(interp::CthulhuInterpreter, override::InferenceResult, optimize::Bool)
@@ -326,11 +337,12 @@ function lookup_constproped_optimized(interp::CthulhuInterpreter, override::Infe
         # e.g. when we switch from constant-prop' unoptimized source
         src = CC.copy(opt.ir)
         rt = override.result
+        exct = override.exc_result
         infos = src.stmts.info
         slottypes = src.argtypes
         codeinf = opt.src
         effects = opt.effects
-        return (; src, rt, infos, slottypes, effects, codeinf)
+        return (; src, rt, exct, infos, slottypes, effects, codeinf)
     else
         # the source might be unavailable at this point,
         # when a result is fully constant-folded etc.
@@ -344,24 +356,25 @@ function lookup_constproped_unoptimized(interp::CthulhuInterpreter, override::In
         unopt = interp.unopt[override.linfo]
     end
     codeinf = src = copy(unopt.src)
-    rt = unopt.rt
+    (; rt, exct) = unopt
     infos = unopt.stmt_info
     effects = get_effects(unopt)
     slottypes = src.slottypes
     if isnothing(slottypes)
         slottypes = Any[ Any for i = 1:length(src.slotflags) ]
     end
-    return (; src, rt, infos, slottypes, effects, codeinf)
+    return (; src, rt, exct, infos, slottypes, effects, codeinf)
 end
 
 function lookup_semiconcrete(interp::CthulhuInterpreter, override::SemiConcreteCallInfo, optimize::Bool)
     src = CC.copy(override.ir)
     rt = get_rt(override)
+    exct = Any # TODO
     infos = src.stmts.info
     slottypes = src.argtypes
     effects = get_effects(override)
     (; codeinf) = lookup(interp, get_mi(override), optimize)
-    return (; src, rt, infos, slottypes, effects, codeinf)
+    return (; src, rt, exct, infos, slottypes, effects, codeinf)
 end
 
 function get_override(@nospecialize(info))
@@ -378,20 +391,21 @@ end
 ##
 function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::AbstractCursor;
     override::Union{Nothing,InferenceResult,SemiConcreteCallInfo} = nothing,
-    debuginfo::Union{Symbol,DebugInfo}       = CONFIG.debuginfo,                     # default is compact debuginfo
-    optimize::Bool                           = CONFIG.optimize,                      # default is true
-    interruptexc::Bool                       = CONFIG.interruptexc,
-    iswarn::Bool                             = CONFIG.iswarn,                        # default is false
-    hide_type_stable::Union{Nothing,Bool}    = CONFIG.hide_type_stable,
-    verbose::Union{Nothing,Bool}             = nothing,
-    remarks::Bool                            = CONFIG.remarks&!CONFIG.optimize,      # default is false
-    with_effects::Bool                       = CONFIG.with_effects,                  # default is false
-    inline_cost::Bool                        = CONFIG.inline_cost&CONFIG.optimize,   # default is false
-    type_annotations::Bool                   = CONFIG.type_annotations,              # default is true
-    annotate_source::Bool                    = CONFIG.annotate_source,               # default is true
-    inlay_types_vscode::Bool                 = CONFIG.inlay_types_vscode,            # default is true
-    diagnostics_vscode::Bool                 = CONFIG.diagnostics_vscode,            # default is true
-    jump_always::Bool                        = CONFIG.jump_always,                   # default is false
+    debuginfo::Union{Symbol,DebugInfo}    = CONFIG.debuginfo,                     # default is compact debuginfo
+    optimize::Bool                        = CONFIG.optimize,                      # default is true
+    interruptexc::Bool                    = CONFIG.interruptexc,
+    iswarn::Bool                          = CONFIG.iswarn,                        # default is false
+    hide_type_stable::Union{Nothing,Bool} = CONFIG.hide_type_stable,
+    verbose::Union{Nothing,Bool}          = nothing,
+    remarks::Bool                         = CONFIG.remarks&!CONFIG.optimize,      # default is false
+    with_effects::Bool                    = CONFIG.with_effects,                  # default is false
+    exception_type::Bool                  = CONFIG.exception_type,                # default is false
+    inline_cost::Bool                     = CONFIG.inline_cost&CONFIG.optimize,   # default is false
+    type_annotations::Bool                = CONFIG.type_annotations,              # default is true
+    annotate_source::Bool                 = CONFIG.annotate_source,               # default is true
+    inlay_types_vscode::Bool              = CONFIG.inlay_types_vscode,            # default is true
+    diagnostics_vscode::Bool              = CONFIG.diagnostics_vscode,            # default is true
+    jump_always::Bool                     = CONFIG.jump_always,                   # default is false
     )
 
     if isnothing(hide_type_stable)
@@ -413,7 +427,9 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
         do_typeinf!(new_interp, new_mi)
         _descend(term, new_interp, new_mi;
                  debuginfo, optimize, interruptexc, iswarn, hide_type_stable, remarks,
-                 with_effects, inline_cost, type_annotations, annotate_source, inlay_types_vscode, diagnostics_vscode)
+                 with_effects, exception_type,
+                 inline_cost, type_annotations, annotate_source,
+                 inlay_types_vscode, diagnostics_vscode)
     end
     custom_toggles = Cthulhu.custom_toggles(interp)
     if !(custom_toggles isa Vector{CustomToggle})
@@ -424,9 +440,9 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
     end
     while true
         if isa(override, InferenceResult)
-            (; src, rt, infos, slottypes, codeinf, effects) = lookup_constproped(interp, curs, override, optimize & !annotate_source)
+            (; src, rt, exct, infos, slottypes, codeinf, effects) = lookup_constproped(interp, curs, override, optimize & !annotate_source)
         elseif isa(override, SemiConcreteCallInfo)
-                (; src, rt, infos, slottypes, codeinf, effects) = lookup_semiconcrete(interp, curs, override, optimize & !annotate_source)
+                (; src, rt, exct, infos, slottypes, codeinf, effects) = lookup_semiconcrete(interp, curs, override, optimize & !annotate_source)
         else
             if optimize && !annotate_source
                 codeinst = get_optimized_codeinst(interp, curs)
@@ -436,7 +452,8 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
                         # but make something up that looks plausible.
                         callsites = Callsite[]
                         if display_CI
-                            callsite = Callsite(-1, MICallInfo(codeinst.def, codeinst.rettype, get_effects(codeinst)), :invoke)
+                            exct = @static VERSION ≥ v"1.11.0-DEV.945" ? codeinst.exct : nothing
+                            callsite = Callsite(-1, MICallInfo(codeinst.def, codeinst.rettype, get_effects(codeinst), exct), :invoke)
                             println(iostream)
                             println(iostream, "│ ─ $callsite")
                             println(iostream, "│  return ", Const(codeinst.rettype_const))
@@ -453,7 +470,7 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
                     end
                 end
             end
-            (; src, rt, infos, slottypes, effects, codeinf) = lookup(interp, curs, optimize & !annotate_source)
+            (; src, rt, exct, infos, slottypes, effects, codeinf) = lookup(interp, curs, optimize & !annotate_source)
         end
         mi = get_mi(curs)
         src = preprocess_ci!(src, mi, optimize & !annotate_source, CONFIG)
@@ -462,7 +479,16 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
         else
             @assert length(src.code) == length(infos)
         end
-        callsites, sourcenodes = find_callsites(interp, src, infos, mi, slottypes, optimize & !annotate_source, annotate_source)
+        infkey = override isa InferenceResult ? override : mi
+        @static if VERSION ≥ v"1.11.0-DEV.1127"
+        pc2excts = exception_type ? get_excts(interp, infkey) : nothing
+        else
+        if exception_type
+            @warn "Statement-wise and call-wise exception type information is available only on v\"1.11.0-DEV.1127\" and later"
+        end
+        pc2excts = nothing
+        end
+        callsites, sourcenodes = find_callsites(interp, src, infos, mi, slottypes, optimize & !annotate_source, annotate_source, pc2excts)
 
         if jump_always
             if isdefined(Main, :VSCodeServer) && Main.VSCodeServer isa Module && isdefined(Main.VSCodeServer, :openfile)
@@ -473,8 +499,8 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
         end
 
         if display_CI
-            pc2remarks = remarks ? get_remarks(interp, override !== nothing ? override : mi) : nothing
-            pc2effects = with_effects ? get_effects(interp, override !== nothing ? override : mi) : nothing
+            pc2remarks = remarks ? get_remarks(interp, infkey) : nothing
+            pc2effects = with_effects ? get_effects(interp, infkey) : nothing
             printstyled(IOContext(iostream, :limit=>true), mi.def, '\n'; bold=true)
             if debuginfo == DInfo.compact
                 str = let debuginfo=debuginfo, src=src, codeinf=codeinf, rt=rt, mi=mi,
@@ -484,11 +510,12 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
                         :color => true,
                         :displaysize => displaysize(iostream), # displaysize doesn't propagate otherwise
                         :SOURCE_SLOTNAMES => codeinf === nothing ? false : Base.sourceinfo_slotnames(codeinf),
-                        :with_effects => with_effects)
+                        :with_effects => with_effects,
+                        :exception_type => exception_type)
                     stringify(ioctx) do lambda_io
-                        cthulhu_typed(lambda_io, debuginfo, annotate_source ? codeinf : src, rt, effects, mi;
+                        cthulhu_typed(lambda_io, debuginfo, annotate_source ? codeinf : src, rt, exct, effects, mi;
                                       iswarn, optimize, hide_type_stable,
-                                      pc2remarks, pc2effects,
+                                      pc2remarks, pc2effects, pc2excts,
                                       inline_cost, type_annotations, annotate_source, inlay_types_vscode, diagnostics_vscode,
                                       jump_always, interp)
                     end
@@ -502,10 +529,11 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
             else
                 lambda_io = IOContext(iostream,
                     :SOURCE_SLOTNAMES => Base.sourceinfo_slotnames(codeinf),
-                    :with_effects => with_effects)
-                cthulhu_typed(lambda_io, debuginfo, src, rt, effects, mi;
+                    :with_effects => with_effects,
+                    :exception_type => exception_type)
+                cthulhu_typed(lambda_io, debuginfo, src, rt, exct, effects, mi;
                               iswarn, optimize, hide_type_stable,
-                              pc2remarks, pc2effects,
+                              pc2remarks, pc2effects, pc2excts,
                               inline_cost, type_annotations, annotate_source, inlay_types_vscode, diagnostics_vscode,
                               jump_always, interp)
             end
@@ -517,9 +545,14 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
         @label show_menu
 
         shown_callsites = annotate_source ? sourcenodes : callsites
-        menu = CthulhuMenu(shown_callsites, with_effects, optimize & !annotate_source, iswarn&get(iostream, :color, false)::Bool, hide_type_stable, custom_toggles; menu_options...)
-        usg = usage(view_cmd, annotate_source, optimize, iswarn, hide_type_stable, debuginfo, remarks, with_effects, inline_cost, type_annotations,
-                    CONFIG.enable_highlighter, inlay_types_vscode, diagnostics_vscode, jump_always, custom_toggles)
+        menu = CthulhuMenu(shown_callsites, with_effects, exception_type,
+                           optimize & !annotate_source,
+                           iswarn&get(iostream, :color, false)::Bool,
+                           hide_type_stable, custom_toggles; menu_options...)
+        usg = usage(view_cmd, annotate_source, optimize, iswarn, hide_type_stable,
+                    debuginfo, remarks, with_effects, exception_type, inline_cost,
+                    type_annotations, CONFIG.enable_highlighter, inlay_types_vscode,
+                    diagnostics_vscode, jump_always, custom_toggles)
         cid = request(term, usg, menu)
         toggle = menu.toggle
 
@@ -563,7 +596,9 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
                         end
                     end
                 end
-                menu = CthulhuMenu(show_sub_callsites, with_effects, optimize & !annotate_source, false, false, custom_toggles; sub_menu=true, menu_options...)
+                menu = CthulhuMenu(show_sub_callsites, with_effects, exception_type,
+                                   optimize & !annotate_source, false, false, custom_toggles;
+                                   sub_menu=true, menu_options...)
                 cid = request(term, "", menu)
                 if cid == length(sub_callsites) + 1
                     continue
@@ -608,7 +643,7 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
                      override = get_override(info), debuginfo,
                      optimize, interruptexc,
                      iswarn, hide_type_stable,
-                     remarks, with_effects, inline_cost,
+                     remarks, with_effects, exception_type, inline_cost,
                      type_annotations, annotate_source,
                      inlay_types_vscode, diagnostics_vscode,
                      jump_always)
@@ -617,6 +652,8 @@ function _descend(term::AbstractTerminal, interp::AbstractInterpreter, curs::Abs
             iswarn ⊻= true
         elseif toggle === :with_effects
             with_effects ⊻= true
+        elseif toggle === :exception_type
+            exception_type ⊻= true
         elseif toggle === :hide_type_stable
             hide_type_stable ⊻= true
         elseif toggle === :inlay_types_vscode
diff --git a/src/callsite.jl b/src/callsite.jl
index 6ac77cc..afa7ce9 100644
--- a/src/callsite.jl
+++ b/src/callsite.jl
@@ -7,17 +7,19 @@ struct MICallInfo <: CallInfo
     mi::MethodInstance
     rt
     effects::Effects
-    function MICallInfo(mi::MethodInstance, @nospecialize(rt), effects)
+    exct
+    function MICallInfo(mi::MethodInstance, @nospecialize(rt), effects, @nospecialize(exct=nothing))
         if isa(rt, LimitedAccuracy)
-            return LimitedCallInfo(new(mi, ignorelimited(rt), effects))
+            return LimitedCallInfo(new(mi, ignorelimited(rt), effects, exct))
         else
-            return new(mi, rt, effects)
+            return new(mi, rt, effects, exct)
         end
     end
 end
 get_mi(ci::MICallInfo) = ci.mi
-get_rt(ci::CallInfo) = ci.rt
+get_rt(ci::MICallInfo) = ci.rt
 get_effects(ci::MICallInfo) = ci.effects
+get_exct(ci::MICallInfo) = ci.exct
 
 abstract type WrappedCallInfo <: CallInfo end
 
@@ -27,6 +29,7 @@ ignorewrappers(ci::WrappedCallInfo) = ignorewrappers(get_wrapped(ci))
 get_mi(ci::WrappedCallInfo) = get_mi(ignorewrappers(ci))
 get_rt(ci::WrappedCallInfo) = get_rt(ignorewrappers(ci))
 get_effects(ci::WrappedCallInfo) = get_effects(ignorewrappers(ci))
+get_exct(ci::WrappedCallInfo) = get_exct(ignorewrappers(ci))
 
 # only appears when inspecting pre-optimization states
 struct LimitedCallInfo <: WrappedCallInfo
@@ -38,9 +41,12 @@ struct RTCallInfo <: CallInfo
     f
     argtyps
     rt
+    exct
 end
+get_rt(ci::RTCallInfo) = ci.rt
 get_mi(ci::RTCallInfo) = nothing
 get_effects(ci::RTCallInfo) = Effects()
+get_exct(ci::RTCallInfo) = ci.exct
 
 # uncached callsite, we can't recurse into this call
 struct UncachedCallInfo <: WrappedCallInfo
@@ -56,6 +62,7 @@ end
 get_mi(::PureCallInfo) = nothing
 get_rt(pci::PureCallInfo) = pci.rt
 get_effects(::PureCallInfo) = EFFECTS_TOTAL
+get_exct(::PureCallInfo) = Union{}
 
 # Failed
 struct FailedCallInfo <: CallInfo
@@ -64,7 +71,8 @@ struct FailedCallInfo <: CallInfo
 end
 get_mi(ci::FailedCallInfo) = fail(ci)
 get_rt(ci::FailedCallInfo) = fail(ci)
-get_effects(ci::FailedCallInfo) = Effects()
+get_effects(ci::FailedCallInfo) = fail(ci)
+get_exct(ci::FailedCallInfo) = fail(ci)
 function fail(ci::FailedCallInfo)
     @warn "MethodInstance extraction failed." ci.sig ci.rt
     return nothing
@@ -77,7 +85,8 @@ struct GeneratedCallInfo <: CallInfo
 end
 get_mi(genci::GeneratedCallInfo) = fail(genci)
 get_rt(genci::GeneratedCallInfo) = fail(genci)
-get_effects(genci::GeneratedCallInfo) = Effects()
+get_effects(genci::GeneratedCallInfo) = fail(genci)
+get_exct(genci::GeneratedCallInfo) = fail(genci)
 function fail(genci::GeneratedCallInfo)
     @warn "Can't extract MethodInstance from call to generated functions." genci.sig genci.rt
     return nothing
@@ -86,11 +95,16 @@ end
 struct MultiCallInfo <: CallInfo
     sig
     rt
+    exct
     callinfos::Vector{CallInfo}
+    MultiCallInfo(@nospecialize(sig), @nospecialize(rt), callinfos::Vector{CallInfo},
+                  @nospecialize(exct=nothing)) =
+        new(sig, rt, exct, callinfos)
 end
-# actual code-error
 get_mi(ci::MultiCallInfo) = error("Can't extract MethodInstance from multiple call informations")
+get_rt(ci::MultiCallInfo) = ci.rt
 get_effects(mci::MultiCallInfo) = mapreduce(get_effects, CC.merge_effects, mci.callinfos)
+get_exct(ci::MultiCallInfo) = ci.exct
 
 struct TaskCallInfo <: CallInfo
     ci::CallInfo
@@ -98,6 +112,7 @@ end
 get_mi(tci::TaskCallInfo) = get_mi(tci.ci)
 get_rt(tci::TaskCallInfo) = get_rt(tci.ci)
 get_effects(tci::TaskCallInfo) = get_effects(tci.ci)
+get_exct(tci::TaskCallInfo) = get_exct(tci.ci)
 
 struct InvokeCallInfo <: CallInfo
     ci::CallInfo
@@ -106,6 +121,7 @@ end
 get_mi(ici::InvokeCallInfo) = get_mi(ici.ci)
 get_rt(ici::InvokeCallInfo) = get_rt(ici.ci)
 get_effects(ici::InvokeCallInfo) = get_effects(ici.ci)
+get_exct(ici::InvokeCallInfo) = get_exct(ici.ci)
 
 # OpaqueClosure CallInfo
 struct OCCallInfo <: CallInfo
@@ -115,6 +131,7 @@ end
 get_mi(occi::OCCallInfo) = get_mi(occi.ci)
 get_rt(occi::OCCallInfo) = get_rt(occi.ci)
 get_effects(occi::OCCallInfo) = get_effects(occi.ci)
+get_exct(occi::OCCallInfo) = get_exct(occi.ci)
 
 # Special handling for ReturnTypeCall
 struct ReturnTypeCallInfo <: CallInfo
@@ -123,6 +140,7 @@ end
 get_mi((; vmi)::ReturnTypeCallInfo) = isa(vmi, FailedCallInfo) ? nothing : get_mi(vmi)
 get_rt((; vmi)::ReturnTypeCallInfo) = Type{isa(vmi, FailedCallInfo) ? Union{} : widenconst(get_rt(vmi))}
 get_effects(::ReturnTypeCallInfo) = EFFECTS_TOTAL
+get_exct(::ReturnTypeCallInfo) = Union{} # FIXME
 
 struct ConstPropCallInfo <: CallInfo
     mi::CallInfo
@@ -131,6 +149,7 @@ end
 get_mi(cpci::ConstPropCallInfo) = cpci.result.linfo
 get_rt(cpci::ConstPropCallInfo) = get_rt(cpci.mi)
 get_effects(cpci::ConstPropCallInfo) = get_effects(cpci.result)
+get_exct(cpci::ConstPropCallInfo) = get_exct(cpci.mi)
 
 struct ConcreteCallInfo <: CallInfo
     mi::CallInfo
@@ -139,6 +158,7 @@ end
 get_mi(ceci::ConcreteCallInfo) = get_mi(ceci.mi)
 get_rt(ceci::ConcreteCallInfo) = get_rt(ceci.mi)
 get_effects(ceci::ConcreteCallInfo) = get_effects(ceci.mi)
+get_exct(cici::ConcreteCallInfo) = get_exct(ceci.mi)
 
 struct SemiConcreteCallInfo <: CallInfo
     mi::CallInfo
@@ -147,6 +167,7 @@ end
 get_mi(scci::SemiConcreteCallInfo) = get_mi(scci.mi)
 get_rt(scci::SemiConcreteCallInfo) = get_rt(scci.mi)
 get_effects(scci::SemiConcreteCallInfo) = get_effects(scci.mi)
+get_exct(scci::SemiConcreteCallInfo) = get_exct(scci.mi)
 
 # CUDA callsite
 struct CuCallInfo <: CallInfo
@@ -187,22 +208,22 @@ function headstring(@nospecialize(T))
     end
 end
 
-function __show_limited(limiter, name, tt, @nospecialize(rt), effects)
+function __show_limited(limiter, name, tt, @nospecialize(rt), effects, @nospecialize(exct=nothing))
     vastring(@nospecialize(T)) = (isvarargtype(T) ? headstring(T)*"..." : string(T)::String)
 
     # If effects are explicitly turned on, make sure to print them, even
     # if there otherwise isn't space for them, since the effects are the
     # most important piece of information if turned on.
     with_effects = get(limiter, :with_effects, false)::Bool
+    exception_type = get(limiter, :exception_type, false)::Bool && exct !== nothing
 
-    if with_effects
-        limiter.width += textwidth(repr(effects)) + 1
-    end
-
+    with_effects && (limiter.width += textwidth(repr(effects)) + 1)
+    exception_type && (limiter.width += textwidth(string(exct)) + 1)
     if !has_space(limiter, name)
         print(limiter, '…')
         @goto print_effects
     end
+
     print(limiter, string(name))
     pstrings = String[vastring(T) for T in tt]
     headstrings = String[
@@ -234,15 +255,28 @@ function __show_limited(limiter, name, tt, @nospecialize(rt), effects)
         print(limiter, "::…")
     end
 
-@label print_effects
+    @label print_effects
     if with_effects
         # Print effects unlimited
         print(limiter.io, " ", effects)
     end
+    if exception_type
+        print(limiter.io, ' ', ExctWrapper(exct))
+    end
 
     return nothing
 end
 
+struct ExctWrapper
+    exct
+    ExctWrapper(@nospecialize exct) = new(exct)
+end
+
+function Base.show(io::IO, (;exct)::ExctWrapper)
+    color = exct === Union{} ? :green : :yellow
+    printstyled(io, "(↑::", exct, ")"; color)
+end
+
 function show_callinfo(limiter, mici::MICallInfo)
     mi = mici.mi
     tt = (Base.unwrap_unionall(mi.specTypes)::DataType).parameters[2:end]
@@ -252,7 +286,8 @@ function show_callinfo(limiter, mici::MICallInfo)
         name = mi.def.name
     end
     rt = get_rt(mici)
-    __show_limited(limiter, name, tt, rt, get_effects(mici))
+    exct = get_exct(mici)
+    __show_limited(limiter, name, tt, rt, get_effects(mici), exct)
 end
 
 function show_callinfo(limiter, ci::Union{MultiCallInfo, FailedCallInfo, GeneratedCallInfo})
diff --git a/src/codeview.jl b/src/codeview.jl
index af897e4..a1ccd4d 100644
--- a/src/codeview.jl
+++ b/src/codeview.jl
@@ -106,7 +106,7 @@ function cthulhu_warntype(io::IO, debuginfo::AnyDebugInfo,
     if inline_cost
         isa(mi, MethodInstance) || error("Need a MethodInstance to show inlining costs. Call `cthulhu_typed` directly instead.")
     end
-    cthulhu_typed(io, debuginfo, src, rt, effects, mi; iswarn=true, optimize, hide_type_stable, inline_cost, interp)
+    cthulhu_typed(io, debuginfo, src, rt, nothing, effects, mi; iswarn=true, optimize, hide_type_stable, inline_cost, interp)
     return nothing
 end
 
@@ -121,9 +121,12 @@ end
 cthulhu_typed(io::IO, debuginfo::DebugInfo, args...; kwargs...) =
     cthulhu_typed(io, Symbol(debuginfo), args...; kwargs...)
 function cthulhu_typed(io::IO, debuginfo::Symbol,
-    src::Union{CodeInfo,IRCode}, @nospecialize(rt), effects::Effects, mi::Union{Nothing,MethodInstance};
+    src::Union{CodeInfo,IRCode}, @nospecialize(rt), @nospecialize(exct),
+    effects::Effects, mi::Union{Nothing,MethodInstance};
     iswarn::Bool=false, hide_type_stable::Bool=false, optimize::Bool=true,
-    pc2remarks::Union{Nothing,PC2Remarks}=nothing, pc2effects::Union{Nothing,PC2Effects}=nothing,
+    pc2remarks::Union{Nothing,PC2Remarks}=nothing,
+    pc2effects::Union{Nothing,PC2Effects}=nothing,
+    pc2excts::Union{Nothing,PC2Excts}=nothing,
     inline_cost::Bool=false, type_annotations::Bool=true, annotate_source::Bool=false,
     inlay_types_vscode::Bool=false, diagnostics_vscode::Bool=false, jump_always::Bool=false,
     interp::AbstractInterpreter=CthulhuInterpreter())
@@ -248,18 +251,28 @@ function cthulhu_typed(io::IO, debuginfo::Symbol,
         end
     end
     # postprinter configuration
-    __postprinter = if type_annotations
+    ___postprinter = if type_annotations
         iswarn ? InteractiveUtils.warntype_type_printer : IRShow.default_expr_type_printer
     else
         Returns(nothing)
     end
-    _postprinter = if isa(src, CodeInfo) && !isnothing(pc2effects)
+    __postprinter = if isa(src, CodeInfo) && !isnothing(pc2effects)
         function (io::IO; idx::Int, @nospecialize(kws...))
-            __postprinter(io; idx, kws...)
+            ___postprinter(io; idx, kws...)
             local effects = get(pc2effects, idx, nothing)
             effects === nothing && return
             print(io, ' ', effects)
         end
+    else
+        ___postprinter
+    end
+    _postprinter = if isa(src, CodeInfo) && !isnothing(pc2excts)
+        function (io::IO; idx::Int, @nospecialize(kws...))
+            __postprinter(io; idx, kws...)
+            local exct = get(pc2excts, idx, nothing)
+            exct === nothing && return
+            print(io, ' ', ExctWrapper(exct))
+        end
     else
         __postprinter
     end
@@ -293,7 +306,7 @@ function cthulhu_typed(io::IO, debuginfo::Symbol,
         cfg = src isa IRCode ? src.cfg : Core.Compiler.compute_basic_blocks(src.code)
         max_bb_idx_size = length(string(length(cfg.blocks)))
         str = irshow_config.line_info_preprinter(lambda_io, " "^(max_bb_idx_size + 2), -1)
-        callsite = Callsite(0, MICallInfo(mi, rettype, effects), :invoke)
+        callsite = Callsite(0, MICallInfo(mi, rettype, effects, exct), :invoke)
         println(lambda_io, "∘ ", "─"^(max_bb_idx_size), str, " ", callsite)
     end
 
@@ -444,7 +457,7 @@ function Base.show(
         return
     end
     println(io, "Cthulhu.Bookmark (world: ", world, ")")
-    cthulhu_typed(io, debuginfo, CI, rt, effects, b.mi; iswarn, optimize, hide_type_stable, b.interp)
+    cthulhu_typed(io, debuginfo, CI, rt, nothing, effects, b.mi; iswarn, optimize, hide_type_stable, b.interp)
 end
 
 function InteractiveUtils.code_typed(b::Bookmark; optimize::Bool=true)
diff --git a/src/interface.jl b/src/interface.jl
index c7e5e04..ba37f03 100644
--- a/src/interface.jl
+++ b/src/interface.jl
@@ -68,13 +68,14 @@ missing `$AbstractCursor` API:
 """)
 navigate(curs::CthulhuCursor, callsite::Callsite) = CthulhuCursor(get_mi(callsite))
 
-get_remarks(::AbstractInterpreter, ::Union{MethodInstance,InferenceResult}) = nothing
-get_remarks(interp::CthulhuInterpreter, key::Union{MethodInstance,InferenceResult}) = get(interp.remarks, key, nothing)
-get_remarks(::AbstractInterpreter, ::SemiConcreteCallInfo) = PC2Remarks()
+get_remarks(::AbstractInterpreter, ::InferenceKey) = nothing
+get_remarks(interp::CthulhuInterpreter, key::InferenceKey) = get(interp.remarks, key, nothing)
 
-get_effects(::AbstractInterpreter, ::Union{MethodInstance,InferenceResult}) = nothing
-get_effects(interp::CthulhuInterpreter, key::Union{MethodInstance,InferenceResult}) = get(interp.effects, key, nothing)
-get_effects(::AbstractInterpreter, ::SemiConcreteCallInfo) = PC2Effects()
+get_effects(::AbstractInterpreter, ::InferenceKey) = nothing
+get_effects(interp::CthulhuInterpreter, key::InferenceKey) = get(interp.effects, key, nothing)
+
+get_excts(::AbstractInterpreter, ::InferenceKey) = nothing
+get_excts(interp::CthulhuInterpreter, key::InferenceKey) = get(interp.exception_types, key, nothing)
 
 # This method is optional, but should be implemented if there is
 # a sensible default cursor for a MethodInstance
diff --git a/src/interpreter.jl b/src/interpreter.jl
index 546a7d4..b03e1d4 100644
--- a/src/interpreter.jl
+++ b/src/interpreter.jl
@@ -9,8 +9,10 @@ struct InferredSource
     stmt_info::Vector{CCCallInfo}
     effects::Effects
     rt::Any
-    InferredSource(src::CodeInfo, stmt_info::Vector{CCCallInfo}, effects, @nospecialize(rt)) =
-        new(src, stmt_info, effects, rt)
+    exct::Any
+    InferredSource(src::CodeInfo, stmt_info::Vector{CCCallInfo}, effects, @nospecialize(rt),
+                   @nospecialize(exct)) =
+        new(src, stmt_info, effects, rt, exct)
 end
 
 struct OptimizedSource
@@ -20,26 +22,31 @@ struct OptimizedSource
     effects::Effects
 end
 
+const InferenceKey = Union{MethodInstance,InferenceResult}
+const InferenceDict{T} = Dict{InferenceKey, T}
 const PC2Remarks = Vector{Pair{Int, String}}
 const PC2Effects = Dict{Int, Effects}
+const PC2Excts = Dict{Int, Any}
 
 struct CthulhuInterpreter <: AbstractInterpreter
     native::AbstractInterpreter
 
-    unopt::Dict{Union{MethodInstance,InferenceResult}, InferredSource}
+    unopt::InferenceDict{InferredSource}
     opt::Dict{MethodInstance, CodeInstance}
 
-    remarks::Dict{Union{MethodInstance,InferenceResult}, PC2Remarks}
-    effects::Dict{Union{MethodInstance,InferenceResult}, PC2Effects}
+    remarks::InferenceDict{PC2Remarks}
+    effects::InferenceDict{PC2Effects}
+    exception_types::InferenceDict{PC2Excts}
 end
 
 function CthulhuInterpreter(interp::AbstractInterpreter=NativeInterpreter())
     return CthulhuInterpreter(
         interp,
-        Dict{Union{MethodInstance,InferenceResult}, InferredSource}(),
+        InferenceDict{InferredSource}(),
         Dict{MethodInstance, CodeInstance}(),
-        Dict{Union{MethodInstance,InferenceResult}, PC2Remarks}(),
-        Dict{Union{MethodInstance,InferenceResult}, PC2Effects}())
+        InferenceDict{PC2Remarks}(),
+        InferenceDict{PC2Effects}(),
+        InferenceDict{PC2Excts}())
 end
 
 import .CC: InferenceParams, OptimizationParams, get_world_counter,
@@ -138,11 +145,13 @@ function InferredSource(state::InferenceState)
             slottypes === nothing ? nothing : copy(slottypes)
         end
     end
+    exct = @static VERSION ≥ v"1.11.0-DEV.207" ? state.result.exc_result : nothing
     return InferredSource(
         unoptsrc,
         copy(state.stmt_info),
         isdefined(CC, :Effects) ? state.ipo_effects : nothing,
-        state.result.result)
+        state.result.result,
+        exct)
 end
 
 function CC.finish(state::InferenceState, interp::CthulhuInterpreter)
@@ -236,3 +245,14 @@ function CC.finish!(interp::CthulhuInterpreter, caller::InferenceResult)
     caller.src = create_cthulhu_source(caller.src, caller.ipo_effects)
 end
 end
+
+@static if VERSION ≥ v"1.11.0-DEV.1127"
+function CC.update_exc_bestguess!(interp::CthulhuInterpreter, @nospecialize(exct),
+                                  frame::InferenceState)
+    key = CC.any(frame.result.overridden_by_const) ? frame.result : frame.linfo
+    pc2excts = get!(PC2Excts, interp.exception_types, key)
+    pc2excts[frame.currpc] = CC.tmerge(CC.typeinf_lattice(interp), exct, get(pc2excts, frame.currpc, Union{}))
+    return @invoke CC.update_exc_bestguess!(interp::AbstractInterpreter, exct::Any,
+                                            frame::InferenceState)
+end
+end
diff --git a/src/reflection.jl b/src/reflection.jl
index 4e3501d..eee0400 100644
--- a/src/reflection.jl
+++ b/src/reflection.jl
@@ -20,7 +20,8 @@ end
 
 function find_callsites(interp::AbstractInterpreter, CI::Union{Core.CodeInfo, IRCode},
                         stmt_infos::Union{Vector{CCCallInfo}, Nothing}, mi::Core.MethodInstance,
-                        slottypes::Vector{Any}, optimize::Bool=true, annotate_source::Bool=false)
+                        slottypes::Vector{Any}, optimize::Bool=true, annotate_source::Bool=false,
+                        pc2excts::Union{Nothing,PC2Excts}=nothing)
     sptypes = sptypes_from_meth_instance(mi)
     callsites, sourcenodes = Callsite[], Union{TypedSyntax.MaybeTypedSyntaxNode,Callsite}[]
     if isa(CI, IRCode)
@@ -53,7 +54,8 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{Core.CodeInfo, IR
                                       t = argextype(arg, CI, sptypes, slottypes)
                                       return ignorelimited(t)
                                   end, args)
-                callinfos = process_info(interp, info, argtypes, rt, optimize)
+                exct = isnothing(pc2excts) ? nothing : get(pc2excts, id, nothing)
+                callinfos = process_info(interp, info, argtypes, rt, optimize, exct)
                 isempty(callinfos) && continue
                 callsite = let
                     if length(callinfos) == 1
@@ -116,7 +118,8 @@ function find_callsites(interp::AbstractInterpreter, CI::Union{Core.CodeInfo, IR
 end
 
 function process_const_info(interp::AbstractInterpreter, @nospecialize(thisinfo),
-    argtypes::ArgTypes, @nospecialize(rt), @nospecialize(result), optimize::Bool)
+    argtypes::ArgTypes, @nospecialize(rt), @nospecialize(result), optimize::Bool,
+    @nospecialize(exct))
     is_cached(@nospecialize(key)) = can_descend(interp, key, optimize)
 
     if isnothing(result)
@@ -128,55 +131,57 @@ function process_const_info(interp::AbstractInterpreter, @nospecialize(thisinfo)
     elseif isa(result, CC.ConcreteResult)
         linfo = result.mi
         effects = get_effects(result)
-        mici = MICallInfo(linfo, rt, effects)
+        mici = MICallInfo(linfo, rt, effects, exct)
         return ConcreteCallInfo(mici, argtypes)
     elseif isa(result, CC.ConstPropResult)
         result = result.result
         linfo = result.linfo
         effects = get_effects(result)
-        mici = MICallInfo(linfo, rt, effects)
+        mici = MICallInfo(linfo, rt, effects, exct)
         return ConstPropCallInfo(is_cached(optimize ? linfo : result) ? mici : UncachedCallInfo(mici), result)
     elseif isa(result, CC.SemiConcreteResult)
         linfo = result.mi
         effects = get_effects(result)
-        mici = MICallInfo(linfo, rt, effects)
+        mici = MICallInfo(linfo, rt, effects, exct)
         return SemiConcreteCallInfo(mici, result.ir)
     else
         @assert isa(result, CC.InferenceResult)
         linfo = result.linfo
         effects = get_effects(result)
-        mici = MICallInfo(linfo, rt, effects)
+        mici = MICallInfo(linfo, rt, effects, exct)
         return ConstPropCallInfo(is_cached(optimize ? linfo : result) ? mici : UncachedCallInfo(mici), result)
     end
 end
 
-function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInfo), argtypes::ArgTypes, @nospecialize(rt), optimize::Bool)
+function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInfo),
+                      argtypes::ArgTypes, @nospecialize(rt), optimize::Bool,
+                      @nospecialize(exct))
     is_cached(@nospecialize(key)) = can_descend(interp, key, optimize)
-    process_recursive(@nospecialize(newinfo)) = process_info(interp, newinfo, argtypes, rt, optimize)
+    process_recursive(@nospecialize(newinfo)) = process_info(interp, newinfo, argtypes, rt, optimize, exct)
 
     if isa(info, MethodResultPure)
         if isa(info.info, CC.ReturnTypeCallInfo)
             # xref: https://github.com/JuliaLang/julia/pull/45299#discussion_r871939049
             info = info.info # cascade to the special handling below
         else
-            return Any[PureCallInfo(argtypes, rt)]
+            return CallInfo[PureCallInfo(argtypes, rt)]
         end
     end
     if isa(info, MethodMatchInfo)
         if info.results === missing
-            return []
+            return CallInfo[]
         end
         matches = info.results.matches
-        return mapany(matches) do match::Core.MethodMatch
+        return CallInfo[let
             mi = specialize_method(match)
             effects = get_effects(interp, mi, false)
-            mici = MICallInfo(mi, rt, effects)
-            return is_cached(mi) ? mici : UncachedCallInfo(mici)
-        end
+            mici = MICallInfo(mi, rt, effects, exct)
+            is_cached(mi) ? mici : UncachedCallInfo(mici)
+        end for match::Core.MethodMatch in matches]
     elseif isa(info, UnionSplitInfo)
-        return mapreduce(process_recursive, vcat, info.matches; init=[])::Vector{Any}
+        return mapreduce(process_recursive, vcat, info.matches; init=CallInfo[])::Vector{CallInfo}
     elseif isa(info, UnionSplitApplyCallInfo)
-        return mapreduce(process_recursive, vcat, info.infos; init=[])::Vector{Any}
+        return mapreduce(process_recursive, vcat, info.infos; init=CallInfo[])::Vector{CallInfo}
     elseif isa(info, ApplyCallInfo)
         # XXX: This could probably use its own info. For now,
         # we ignore any implicit iterate calls.
@@ -184,32 +189,32 @@ function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInf
     elseif isa(info, ConstCallInfo)
         infos = process_recursive(info.call)
         @assert length(infos) == length(info.results)
-        return mapany(enumerate(info.results)) do (i, result)
-            process_const_info(interp, infos[i], argtypes, rt, result, optimize)
-        end
+        return CallInfo[let
+            process_const_info(interp, infos[i], argtypes, rt, result, optimize, exct)
+        end for (i, result) in enumerate(info.results)]
     elseif isa(info, CC.InvokeCallInfo)
         mi = specialize_method(info.match; preexisting=true)
         effects = get_effects(interp, mi, false)
         thisinfo = MICallInfo(mi, rt, effects)
-        innerinfo = process_const_info(interp, thisinfo, argtypes, rt, info.result, optimize)
+        innerinfo = process_const_info(interp, thisinfo, argtypes, rt, info.result, optimize, exct)
         info = InvokeCallInfo(innerinfo)
-        return Any[info]
+        return CallInfo[info]
     elseif isa(info, CC.OpaqueClosureCallInfo)
         mi = specialize_method(info.match; preexisting=true)
         effects = get_effects(interp, mi, false)
         thisinfo = MICallInfo(mi, rt, effects)
-        innerinfo = process_const_info(interp, thisinfo, argtypes, rt, info.result, optimize)
+        innerinfo = process_const_info(interp, thisinfo, argtypes, rt, info.result, optimize, exct)
         info = OCCallInfo(innerinfo)
-        return Any[info]
+        return CallInfo[info]
     elseif isa(info, CC.OpaqueClosureCreateInfo)
         # TODO: Add ability to descend into OCs at creation site
-        return []
+        return CallInfo[]
     elseif isa(info, CC.FinalizerInfo)
         # TODO: Add ability to descend into finalizers at creation site
-        return []
+        return CallInfo[]
     elseif isa(info, CC.ReturnTypeCallInfo)
         newargtypes = argtypes[2:end]
-        callinfos = process_info(interp, info.info, newargtypes, unwrapType(widenconst(rt)), optimize)
+        callinfos = process_info(interp, info.info, newargtypes, unwrapType(widenconst(rt)), optimize, exct)
         if length(callinfos) == 1
             vmi = only(callinfos)
         else
@@ -218,13 +223,13 @@ function process_info(interp::AbstractInterpreter, @nospecialize(info::CCCallInf
             sig = Tuple{widenconst(newargtypes[1]), argt.parameters...}
             vmi = FailedCallInfo(sig, Union{})
         end
-        return Any[ReturnTypeCallInfo(vmi)]
+        return CallInfo[ReturnTypeCallInfo(vmi)]
     elseif info == NoCallInfo()
         f = unwrapconst(argtypes[1])
-        isa(f, Core.Builtin) && return []
-        return [RTCallInfo(f, argtypes[2:end], rt)]
+        isa(f, Core.Builtin) && return CallInfo[]
+        return CallInfo[RTCallInfo(f, argtypes[2:end], rt, exct)]
     elseif info === false
-        return []
+        return CallInfo[]
     else
         @eval Main begin
             interp = $interp
@@ -352,7 +357,6 @@ function get_typed_sourcetext(mi::MethodInstance, src::CodeInfo, @nospecialize(r
     meth = mi.def::Method
     tsn, mappings = TypedSyntax.tsn_and_mappings(meth, src, rt; warn, strip_macros=true)
     return truncate_if_defaultargs!(tsn, mappings, meth)
-    return tsn, mappings
 end
 
 function get_typed_sourcetext(mi::MethodInstance, ::IRCode, @nospecialize(rt); kwargs...)
diff --git a/src/ui.jl b/src/ui.jl
index df02462..18ff7ee 100644
--- a/src/ui.jl
+++ b/src/ui.jl
@@ -13,7 +13,7 @@ mutable struct CthulhuMenu <: TerminalMenus.ConfiguredMenu{TerminalMenus.Config}
     custom_toggles::Vector{CustomToggle}
 end
 
-function show_as_line(callsite::Callsite, with_effects::Bool, optimize::Bool, iswarn::Bool)
+function show_as_line(callsite::Callsite, with_effects::Bool, exception_type::Bool, optimize::Bool, iswarn::Bool)
     reduced_displaysize = displaysize(stdout)::Tuple{Int,Int} .- (0, 3)
     sprint() do io
         show(IOContext(io,
@@ -21,15 +21,17 @@ function show_as_line(callsite::Callsite, with_effects::Bool, optimize::Bool, is
             :displaysize  => reduced_displaysize,
             :optimize     => optimize,
             :iswarn       => iswarn,
-            :color        => iswarn | with_effects,
-            :with_effects => with_effects),
+            :color        => iswarn | with_effects | exception_type,
+            :with_effects => with_effects,
+            :exception_type => exception_type),
             callsite)
     end
 end
 
-function CthulhuMenu(callsites, with_effects::Bool, optimize::Bool, iswarn::Bool, hide_type_stable::Bool,
+function CthulhuMenu(callsites, with_effects::Bool, exception_type::Bool,
+                     optimize::Bool, iswarn::Bool, hide_type_stable::Bool,
                      custom_toggles::Vector{CustomToggle}; pagesize::Int=10, sub_menu = false, kwargs...)
-    options = build_options(callsites, with_effects, optimize, iswarn, hide_type_stable)
+    options = build_options(callsites, with_effects, exception_type, optimize, iswarn, hide_type_stable)
     length(options) < 1 && error("CthulhuMenu must have at least one option")
 
     # if pagesize is -1, use automatic paging
@@ -47,15 +49,15 @@ function CthulhuMenu(callsites, with_effects::Bool, optimize::Bool, iswarn::Bool
     return CthulhuMenu(options, pagesize, pageoffset, selected, nothing, sub_menu, config, custom_toggles)
 end
 
-build_options(callsites::Vector{Callsite}, with_effects::Bool, optimize::Bool, iswarn::Bool, ::Bool) =
-    vcat(map(callsite->show_as_line(callsite, with_effects, optimize, iswarn), callsites), ["↩"])
-function build_options(callsites, with_effects::Bool, optimize::Bool, iswarn::Bool, hide_type_stable::Bool)
+build_options(callsites::Vector{Callsite}, with_effects::Bool, exception_type::Bool, optimize::Bool, iswarn::Bool, ::Bool) =
+    vcat(map(callsite->show_as_line(callsite, with_effects, exception_type, optimize, iswarn), callsites), ["↩"])
+function build_options(callsites, with_effects::Bool, exception_type::Bool, optimize::Bool, iswarn::Bool, hide_type_stable::Bool)
     reduced_displaysize::Int = (displaysize(stdout)::Tuple{Int,Int})[2] - 3
     nd::Int = -1
 
     shown_callsites = map(callsites) do node
         if isa(node, Callsite)
-            show_as_line(node, with_effects, optimize, iswarn)
+            show_as_line(node, with_effects, exception_type, optimize, iswarn)
         else
             if nd == -1
                 nd = TypedSyntax.ndigits_linenumbers(node)
@@ -92,8 +94,10 @@ function stringify(@nospecialize(f), context::IOContext)
 end
 
 const debugcolors = (:nothing, :light_black, :yellow)
-function usage(@nospecialize(view_cmd), annotate_source, optimize, iswarn, hide_type_stable, debuginfo, remarks, with_effects, inline_cost, type_annotations, highlight, 
-    inlay_types_vscode, diagnostics_vscode, jump_always, custom_toggles::Vector{CustomToggle})
+function usage(@nospecialize(view_cmd), annotate_source, optimize, iswarn, hide_type_stable,
+               debuginfo, remarks, with_effects, exception_type, inline_cost,
+               type_annotations, highlight, inlay_types_vscode, diagnostics_vscode,
+               jump_always, custom_toggles::Vector{CustomToggle})
     colorize(use_color::Bool, c::Char) = stringify() do io
         use_color ? printstyled(io, c; color=:cyan) : print(io, c)
     end
@@ -123,7 +127,8 @@ function usage(@nospecialize(view_cmd), annotate_source, optimize, iswarn, hide_
                 printstyled(io, 'd'; color=debugcolors[Int(debuginfo)+1])
             end, "]ebuginfo, [",
             colorize(remarks, 'r'), "]emarks, [",
-            colorize(with_effects, 'e'), "]ffects, [",
+            colorize(with_effects, 'e'), "]ffects, ",
+            "e[", colorize(exception_type, 'x'), "]ception types, [",
             colorize(inline_cost, 'i'), "]nlining costs")
     end
     for i = 1:length(custom_toggles)
@@ -157,6 +162,7 @@ const TOGGLES = Dict(
     UInt32('d') => :debuginfo,
     UInt32('r') => :remarks,
     UInt32('e') => :with_effects,
+    UInt32('x') => :exception_type,
     UInt32('i') => :inline_cost,
     UInt32('t') => :type_annotations,
     UInt32('s') => :highlighter,


Test patch:
diff --git a/test/generate_irshow.jl b/test/generate_irshow.jl
index 0e2ddc1..2a95722 100644
--- a/test/generate_irshow.jl
+++ b/test/generate_irshow.jl
@@ -11,7 +11,7 @@ function generate_test_cases(f, tt, fname=string(nameof(f)))
     outputs = Dict()
     tf = (true, false)
     for optimize in tf
-        (; src, infos, mi, rt, effects, slottypes) = cthulhu_info(f, tt; optimize);
+        (; src, infos, mi, rt, exct, effects, slottypes) = cthulhu_info(f, tt; optimize);
         for (debuginfo, iswarn, hide_type_stable, inline_cost, type_annotations) in Iterators.product(
             instances(Cthulhu.DInfo.DebugInfo), tf, tf, tf, tf,
         )
@@ -20,7 +20,7 @@ function generate_test_cases(f, tt, fname=string(nameof(f)))
 
             s = sprint(; context=:color=>true) do io
                 Cthulhu.cthulhu_typed(io, debuginfo,
-                                      src, rt, effects, mi;
+                                      src, rt, exct, effects, mi;
                                       iswarn, hide_type_stable, inline_cost, type_annotations)
             end
             s = strip_base_linenums(s)
diff --git a/test/setup.jl b/test/setup.jl
index 90d9c06..8e0dfd8 100644
--- a/test/setup.jl
+++ b/test/setup.jl
@@ -5,11 +5,11 @@ end
 
 function cthulhu_info(@nospecialize(f), @nospecialize(TT=()); optimize=true)
     (interp, mi) = Cthulhu.mkinterp(Core.Compiler.NativeInterpreter(), f, TT)
-    (; src, rt, infos, slottypes, effects) = Cthulhu.lookup(interp, mi, optimize; allow_no_src=true)
+    (; src, rt, exct, infos, slottypes, effects) = Cthulhu.lookup(interp, mi, optimize; allow_no_src=true)
     if src !== nothing
         src = Cthulhu.preprocess_ci!(src, mi, optimize, Cthulhu.CthulhuConfig(dead_code_elimination=true))
     end
-    (; interp, src, infos, mi, rt, slottypes, effects)
+    (; interp, src, infos, mi, rt, exct, slottypes, effects)
 end
 
 function find_callsites_by_ftt(@nospecialize(f), @nospecialize(TT=Tuple{}); optimize=true)
diff --git a/test/test_Cthulhu.jl b/test/test_Cthulhu.jl
index 4951abc..2f90225 100644
--- a/test/test_Cthulhu.jl
+++ b/test/test_Cthulhu.jl
@@ -357,11 +357,7 @@ end
         callsites, _ = Cthulhu.find_callsites(interp, src, infos, mi, slottypes, false)
         @test isa(callsites[1].info, Cthulhu.SemiConcreteCallInfo)
         @test occursin("= < semi-concrete eval > getproperty(::ComplexF64,::Core.Const(:re))::Float64", string(callsites[1]))
-
         @test Cthulhu.get_rt(callsites[end].info) == Core.Const(sin(1.0))
-
-        @test Cthulhu.get_remarks(interp, callsites[1].info) == Cthulhu.PC2Remarks()
-        @test Cthulhu.get_effects(interp, callsites[1].info) == Cthulhu.PC2Effects()
     end
 end
 
@@ -677,9 +673,9 @@ end
         end
     end
     function doprint(f)
-        (; src, mi, rt, effects) = cthulhu_info(f)
+        (; src, mi, rt, exct, effects) = cthulhu_info(f)
         io = IOBuffer()
-        Cthulhu.cthulhu_typed(io, :none, src, rt, effects, mi; iswarn=false)
+        Cthulhu.cthulhu_typed(io, :none, src, rt, exct, effects, mi; iswarn=false)
         return String(take!(io))
     end
     @test occursin("invoke f1()::…\n", doprint(getfield(m, :f1)))
diff --git a/test/test_codeview.jl b/test/test_codeview.jl
index 02058fd..8e53101 100644
--- a/test/test_codeview.jl
+++ b/test/test_codeview.jl
@@ -10,7 +10,7 @@ using .TestCodeViewSandbox
 Revise.track(TestCodeViewSandbox, normpath(@__DIR__, "TestCodeViewSandbox.jl"))
 
 @testset "printer test" begin
-    (; interp, src, infos, mi, rt, effects, slottypes) = cthulhu_info(testf_revise);
+    (; interp, src, infos, mi, rt, exct, effects, slottypes) = cthulhu_info(testf_revise);
     tf = (true, false)
 
     @testset "codeview: $codeview" for codeview in Cthulhu.CODEVIEWS
@@ -35,7 +35,7 @@ Revise.track(TestCodeViewSandbox, normpath(@__DIR__, "TestCodeViewSandbox.jl"))
                     @testset "type_annotations: $type_annotations" for type_annotations in tf
                         io = IOBuffer()
                         Cthulhu.cthulhu_typed(io, debuginfo,
-                            src, rt, effects, mi;
+                            src, rt, exct, effects, mi;
                             iswarn, hide_type_stable, inline_cost, type_annotations)
                         @test !isempty(String(take!(io))) # just check it works
                     end
@@ -47,7 +47,7 @@ end
 
 @testset "hide type-stable statements" begin
     let # optimize code
-        (; src, infos, mi, rt, effects, slottypes) = @eval Module() begin
+        (; src, infos, mi, rt, exct, effects, slottypes) = @eval Module() begin
             const globalvar = Ref(42)
             $cthulhu_info() do
                 a = sin(globalvar[])
@@ -57,7 +57,7 @@ end
         end
         function prints(; kwargs...)
             io = IOBuffer()
-            Cthulhu.cthulhu_typed(io, :none, src, rt, effects, mi; kwargs...)
+            Cthulhu.cthulhu_typed(io, :none, src, rt, exct, effects, mi; kwargs...)
             return String(take!(io))
         end
 
@@ -74,7 +74,7 @@ end
     end
 
     let # unoptimize code
-        (; src, infos, mi, rt, effects, slottypes) = @eval Module() begin
+        (; src, infos, mi, rt, exct, effects, slottypes) = @eval Module() begin
             const globalvar = Ref(42)
             $cthulhu_info(; optimize=false) do
                 a = sin(globalvar[])
@@ -84,7 +84,7 @@ end
         end
         function prints(; kwargs...)
             io = IOBuffer()
-            Cthulhu.cthulhu_typed(io, :none, src, rt, effects, mi; kwargs...)
+            Cthulhu.cthulhu_typed(io, :none, src, rt, exct, effects, mi; kwargs...)
             return String(take!(io))
         end
 
diff --git a/test/test_irshow.jl b/test/test_irshow.jl
index 22e2870..b9418a1 100644
--- a/test/test_irshow.jl
+++ b/test/test_irshow.jl
@@ -10,7 +10,7 @@ include("IRShowSandbox.jl")
     tf = (true, false)
 
     @testset "optimize: $optimize" for optimize in tf
-        (; src, infos, mi, rt, effects, slottypes) = cthulhu_info(IRShowSandbox.foo, (Int, Int); optimize);
+        (; src, infos, mi, rt, exct, effects, slottypes) = cthulhu_info(IRShowSandbox.foo, (Int, Int); optimize);
 
         @testset "debuginfo: $debuginfo" for debuginfo in instances(Cthulhu.DInfo.DebugInfo)
             @testset "iswarn: $iswarn" for iswarn in tf
@@ -22,7 +22,7 @@ include("IRShowSandbox.jl")
 
                             s = sprint(; context=:color=>true) do io
                                 Cthulhu.cthulhu_typed(io, debuginfo,
-                                                      src, rt, effects, mi;
+                                                      src, rt, exct, effects, mi;
                                                       iswarn, hide_type_stable, inline_cost, type_annotations)
                             end
                             s = strip_base_linenums(s)