Repository: JuliaDebug/Cthulhu.jl
Pull request #181 (2021-07-30 11:03:29)

Fix printing bugs
There was some nice functionality built into `TextWidthLimiter`,
but there was a lot that didn't quite work as expected due to a
lack of tests. This adds new tests, fixes bugs, and introduces
two changes to printing:
- print `::Vararg{Char}` as `::Char...` (with a more widely spaced
  three-dots than used for the "omission" marker
- use "::…" as the return-type annotation when there isn't space
  for the full thing but for which there is room for 3 characters.

If folks don't like these changes, I can revert them.

codecov-commenter: # [Codecov](https://codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/181?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#181](https://codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/181?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (bc14abb) into [master](https://codecov.io/gh/JuliaDebug/Cthulhu.jl/commit/349fb73e0b701d2bfb014224c19227c59d6b745b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (349fb73) will **increase** coverage by `3.54%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/181/graphs/tree.svg?width=650&height=150&src=pr&token=05XW2wLDoq&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)](https://codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/181?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)

```diff
@@            Coverage Diff             @@
##           master     #181      +/-   ##
==========================================
+ Coverage   48.78%   52.33%   +3.54%     
==========================================
  Files           7        7              
  Lines         906      921      +15     
==========================================
+ Hits          442      482      +40     
+ Misses        464      439      -25     
```


| [Impacted Files](https://codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/181?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [src/callsite.jl](https://codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/181/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-c3JjL2NhbGxzaXRlLmps) | `59.77% <100.00%> (+18.92%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/181?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/181?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Last update [349fb73...bc14abb](https://codecov.io/gh/JuliaDebug/Cthulhu.jl/pull/181?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?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 bdd1348..7655108 100644
--- a/src/Cthulhu.jl
+++ b/src/Cthulhu.jl
@@ -214,8 +214,10 @@ function _descend(interp::CthulhuInterpreter, mi::MethodInstance; override::Unio
         debuginfo = getfield(DInfo, selected)::DebugInfo
     end
 
-    display_CI = true
     is_cached(key) = haskey(optimize ? interp.opt : interp.unopt, key)
+
+    menu_options = (cursor = '•', scroll_wrap = true)
+    display_CI = true
     while true
         debuginfo_key = Symbol(debuginfo)
 
@@ -258,8 +260,7 @@ function _descend(interp::CthulhuInterpreter, mi::MethodInstance; override::Unio
             display_CI = true
         end
 
-        TerminalMenus.config(cursor = '•', scroll = :wrap)
-        menu = CthulhuMenu(callsites, optimize)
+        menu = CthulhuMenu(callsites, optimize; menu_options...)
         cid = request(menu)
         toggle = menu.toggle
 
@@ -279,7 +280,7 @@ function _descend(interp::CthulhuInterpreter, mi::MethodInstance; override::Unio
                     @warn "Expected multiple callsites, but found none. Please fill an issue with a reproducing example" info
                     continue
                 end
-                menu = CthulhuMenu(sub_callsites, optimize; sub_menu=true)
+                menu = CthulhuMenu(sub_callsites, optimize; sub_menu=true, menu_options...)
                 cid = request(menu)
                 if cid == length(sub_callsites) + 1
                     continue
@@ -295,7 +296,7 @@ function _descend(interp::CthulhuInterpreter, mi::MethodInstance; override::Unio
             # forcibly enter and inspect the frame, although the native interpreter gave up
             if info isa UncachedCallInfo || info isa TaskCallInfo
                 next_interp = CthulhuInterpreter()
-                next_mi = get_mi(info)
+                next_mi = get_mi(info)::MethodInstance
                 do_typeinf!(next_interp, next_mi)
                 _descend(next_interp, next_mi;
                          params, optimize, iswarn, debuginfo=debuginfo_key, interruptexc, verbose, kwargs...)
@@ -358,7 +359,8 @@ function _descend(interp::CthulhuInterpreter, mi::MethodInstance; override::Unio
             if mod !== nothing
                 revise = getfield(mod, :revise)::Function
                 revise()
-                mi = first_method_instance(mi.specTypes)
+                mi = get_specialization(mi.specTypes)
+                do_typeinf!(interp, mi)
             end
         elseif toggle === :edit
             edit(whereis(mi.def)...)
@@ -373,7 +375,7 @@ function _descend(interp::CthulhuInterpreter, mi::MethodInstance; override::Unio
     end
 end
 
-function do_typeinf!(interp, mi)
+function do_typeinf!(interp::CthulhuInterpreter, mi::MethodInstance)
     result = InferenceResult(mi)
     frame = InferenceState(result, true, interp)
     Core.Compiler.typeinf(interp, frame)
diff --git a/src/backedges.jl b/src/backedges.jl
index 4d7208a..4dbf2ef 100644
--- a/src/backedges.jl
+++ b/src/backedges.jl
@@ -1,6 +1,6 @@
 # A lightly-modified version of the same function in Base
 # Highlights argument types with color specified by highlighter(typ)
-function show_tuple_as_call(@nospecialize(highlighter), io::IO, name::Symbol, @nospecialize(sig::Type), demangle=false, kwargs=nothing)
+function show_tuple_as_call(@nospecialize(highlighter), io::IO, name::Symbol, @nospecialize(sig::Type), demangle=false #=, kwargs=nothing =#)
     if sig === Tuple
         print(io, demangle ? Base.demangle_function_name(name) : name, "(...)")
         return
@@ -34,16 +34,16 @@ function show_tuple_as_call(@nospecialize(highlighter), io::IO, name::Symbol, @n
         first = false
         printstyled(env_io, "::", sig[i], color=highlighter(sig[i]))
     end
-    if kwargs !== nothing
-        print(io, "; ")
-        first = true
-        for (k, t) in kwargs
-            first || print(io, ", ")
-            first = false
-            print(io, k, "::")
-            show(io, t)
-        end
-    end
+    # if kwargs !== nothing
+    #     print(io, "; ")
+    #     first = true
+    #     for (k, t) in kwargs
+    #         first || print(io, ", ")
+    #         first = false
+    #         print(io, k, "::")
+    #         show(io, t)
+    #     end
+    # end
     print(io, ")")
     Base.show_method_params(io, tv)
     nothing
@@ -86,10 +86,10 @@ specTypes(mi::MethodInstance) = mi.specTypes
 instance(mi::MethodInstance) = mi
 nextnode(mi, edge) = edge
 
-instance(sfs::Vector{StackTraces.StackFrame}) = sfs[end].linfo
+instance(sfs::Vector{StackTraces.StackFrame}) = isempty(sfs) ? Core.Compiler.Timings.ROOTmi : sfs[end].linfo
 method(sfs::Vector{StackTraces.StackFrame}) = method(instance(sfs))
 
-instance(ipframes::Vector{IPFrames}) = instance(ipframes[1].sfs)
+instance(ipframes::Vector{IPFrames}) = isempty(ipframes) ? Core.Compiler.Timings.ROOTmi : instance(ipframes[1].sfs)
 backedges(ipframes::Vector{IPFrames}) = (ret = ipframes[2:end]; isempty(ret) ? () : (ret,))
 
 function callstring(io, mi)
@@ -97,6 +97,7 @@ function callstring(io, mi)
     return String(take!(io))
 end
 function callstring(io, sfs::Vector{StackTraces.StackFrame})
+    isempty(sfs) && return ""
     for i = 1:length(sfs)-1
         sf = sfs[i]
         print(io, sf.func, " at ", sf.file, ':', sf.line, " => ")
@@ -104,7 +105,7 @@ function callstring(io, sfs::Vector{StackTraces.StackFrame})
     sf = sfs[end]
     return callstring(io, instance(sfs)) * string(" at ", sf.file, ':', sf.line)
 end
-callstring(io, ipframes::Vector{IPFrames}) = callstring(io, ipframes[1].sfs)
+callstring(io, ipframes::Vector{IPFrames}) = isempty(ipframes) ? "" : callstring(io, ipframes[1].sfs)
 
 struct Data{T}
     callstr::String
diff --git a/src/callsite.jl b/src/callsite.jl
index ade2c46..8241a72 100644
--- a/src/callsite.jl
+++ b/src/callsite.jl
@@ -33,6 +33,14 @@ struct UncachedCallInfo <: WrappedCallInfo
     wrapped::CallInfo
 end
 
+struct PureCallInfo <: CallInfo
+    argtypes::Vector{Any}
+    rt
+    PureCallInfo(argtypes::Vector{Any}, @nospecialize(rt)) =
+        new(argtypes, rt)
+end
+get_mi(::PureCallInfo) = nothing
+
 # Failed
 struct FailedCallInfo <: CallInfo
     sig
@@ -115,6 +123,7 @@ end
 TextWidthLimiter(io::IO, limit) = TextWidthLimiter(io, 0, limit)
 has_space(limiter::TextWidthLimiter, width::Int) = limiter.width + width < limiter.limit - 1
 has_space(limiter::TextWidthLimiter, s) = has_space(limiter, textwidth(string(s)))
+has_space(::IO, s) = true
 function Base.print(io::TextWidthLimiter, s::String)
     io.width == io.limit && return 0
     width = textwidth(s::String)
@@ -126,17 +135,32 @@ function Base.print(io::TextWidthLimiter, s::String)
         for c in graphemes(s)
             cwidth = textwidth(c)
             if has_space(io, cwidth)
-                print(io, c)
+                print(io.io, c)
                 io.width += cwidth
             else
                 break
             end
         end
-        print(io, '…')
+        print(io.io, '…')
         io.width += 1
     end
 end
 
+function Base.print(io::TextWidthLimiter, c::Char)
+    tw = textwidth(c)
+    if has_space(io, tw)
+        print(io.io, c)
+        io.width += tw
+        return
+    end
+    return
+end
+
+function Base.take!(io::TextWidthLimiter)
+    io.width = 0
+    return take!(io.io)
+end
+
 function headstring(@nospecialize(T))
     T = widenconst(Base.unwrapva(T))
     if T isa Union || T === Union{}
@@ -144,18 +168,20 @@ function headstring(@nospecialize(T))
     elseif T isa UnionAll
         return headstring(Base.unwrap_unionall(T))
     else
-        return string(T.name)
+        return string(T.name.name)
     end
 end
 
 
 function __show_limited(limiter, name, tt, rt)
+    vastring(@nospecialize(T)) = Base.isvarargtype(T) ? headstring(T)*"..." : string(T)
+
     if !has_space(limiter, name)
         print(limiter, '…')
         return
     end
     print(limiter, string(name))
-    pstrings = map(string, tt)
+    pstrings = map(vastring, tt)
     headstrings = map(headstring, tt)
     print(limiter, "(")
     if length(pstrings) != 0
@@ -178,7 +204,10 @@ function __show_limited(limiter, name, tt, rt)
     rts = string(rt)
     if has_space(limiter, textwidth(rts)+2)
         print(limiter, string("::", rts))
+    elseif has_space(limiter, 3)
+        print(limiter, "::…")
     end
+    return
 end
 
 function show_callinfo(limiter, mici::MICallInfo)
@@ -202,6 +231,13 @@ function show_callinfo(limiter, ci::Union{MultiCallInfo, FailedCallInfo, Generat
     __show_limited(limiter, name, tt, rt)
 end
 
+function show_callinfo(limiter, (; argtypes, rt)::PureCallInfo)
+    ft, tt... = argtypes
+    f = Compiler.argtype_to_function(ft)
+    name = isnothing(f) ? "unknown" : nameof(f)
+    __show_limited(limiter, name, tt, rt)
+end
+
 function show_callinfo(limiter, ci::ConstPropCallInfo)
     # XXX: The first argument could be const-overriden too
     name = ci.result.linfo.def.name
@@ -222,6 +258,9 @@ function Base.show(io::IO, c::Callsite)
     elseif isa(info, WrappedCallInfo)
         wrapped_callinfo(limiter, info)
         show_callinfo(limiter, ignorewrappers(info))
+    elseif isa(info, PureCallInfo)
+        print(limiter, " = < pure > ")
+        show_callinfo(limiter, info)
     elseif info isa MultiCallInfo
         print(limiter, " = call ")
         show_callinfo(limiter, info)
diff --git a/src/reflection.jl b/src/reflection.jl
index 530d2ca..0433c64 100644
--- a/src/reflection.jl
+++ b/src/reflection.jl
@@ -12,22 +12,6 @@ const sptypes_from_meth_instance = Core.Compiler.sptypes_from_meth_instance
 const may_invoke_generator = Base.may_invoke_generator
 code_for_method(method, metharg, methsp, world, force=false) = Core.Compiler.specialize_method(method, metharg, methsp, force)
 
-# https://github.com/JuliaLang/julia/pull/36318
-_id(x) = x.id
-_id(x::Core.Argument) = x.n
-const SlotOrArgument = Union{Core.Argument,Core.Slot}
-
-function unwrapconst(a)
-    if isa(a, Const)
-        a = Core.Typeof(a.val)
-    elseif isa(a, Core.Compiler.PartialStruct)
-        a = a.typ
-    elseif isa(a, Core.Compiler.MaybeUndef)
-        a = a.typ
-    end
-    return a
-end
-
 transform(::Val, callsite) = callsite
 function transform(::Val{:CuFunction}, callsite, callexpr, CI, mi, slottypes; params=nothing, kwargs...)
     sptypes = sptypes_from_meth_instance(mi)
@@ -51,11 +35,16 @@ function find_callsites(interp::CthulhuInterpreter, CI::Union{Core.CodeInfo, IRC
             info = stmt_info[id]
             if info !== nothing
                 rt = argextype(SSAValue(id), CI, sptypes, slottypes)
-                was_return_type = false
-                if isa(info, MethodResultPure)
-                    # TODO: We could annotate this in the UI
-                    continue
+                # in unoptimized IR, there may be `slot = rhs` expressions, which `argextype` doesn't handle
+                # so extract rhs for such an case
+                args = c.args
+                if !optimize
+                    if isexpr(c, :(=))
+                        args = c.args[2].args
+                    end
                 end
+                types = mapany(arg -> widenconst(argextype(arg, CI, sptypes, slottypes)), args)
+                was_return_type = false
                 if isa(info, Core.Compiler.ReturnTypeCallInfo)
                     info = info.info
                     was_return_type = true
@@ -73,6 +62,8 @@ function find_callsites(interp::CthulhuInterpreter, CI::Union{Core.CodeInfo, IRC
                             mici = MICallInfo(mi, rt)
                             return is_cached(mi) ? mici : UncachedCallInfo(mici)
                         end
+                    elseif isa(info, MethodResultPure)
+                        return Any[PureCallInfo(types, rt)]
                     elseif isa(info, UnionSplitInfo)
                         return mapreduce(process_info, vcat, info.matches)
                     elseif isa(info, UnionSplitApplyCallInfo)
@@ -117,16 +108,7 @@ function find_callsites(interp::CthulhuInterpreter, CI::Union{Core.CodeInfo, IRC
                     if length(callinfos) == 1
                         callinfo = callinfos[1]
                     else
-                        # in unoptimized IR, there may be `slot = rhs` expressions, which `argextype` doesn't handle
-                        # so extract rhs for such an case
-                        args = c.args
-                        if !optimize
-                            if isexpr(c, :(=))
-                                args = c.args[2].args
-                            end
-                        end
-                        types = mapany(arg -> widenconst(argextype(arg, CI, sptypes, slottypes)), args)
-                        callinfo = MultiCallInfo(Core.Compiler.argtypes_to_type(types), rt, callinfos)
+                        callinfo = MultiCallInfo(Compiler.argtypes_to_type(types), rt, callinfos)
                     end
                     if was_return_type
                         callinfo = ReturnTypeCallInfo(callinfo)
@@ -220,29 +202,6 @@ end
 const CompilerParams = Core.Compiler.NativeInterpreter
 current_params() = CompilerParams()
 
-function first_method_instance(@nospecialize(F), @nospecialize(TT); params=current_params())
-    sig = Base.signature_type(F, TT)
-    first_method_instance(sig; params=params)
-end
-
-function first_method_instance(@nospecialize(sig); params=current_params())
-    ci = callinfo(sig, Any, 1, params=params)
-    if ci isa Union{GeneratedCallInfo, FailedCallInfo, MultiCallInfo}
-        if ci isa MultiCallInfo
-            @warn "expected single CallInfo got multiple" ci
-        end
-        return nothing
-    else
-        @assert ci isa MICallInfo
-        return get_mi(ci)
-    end
-end
-
-function choose_method_instance(@nospecialize(F), @nospecialize(TT); params=current_params())
-    sig = Base.signature_type(F, TT)
-    choose_method_instance(sig; params=params)
-end
-
 function callinfo(sig, rt, max_methods=-1; params=current_params())
     methds = Base._methods_by_ftype(sig, max_methods, params.world)
     (methds === false || length(methds) < 1) && return FailedCallInfo(sig, rt)
diff --git a/src/ui.jl b/src/ui.jl
index eab76d9..9bc6113 100644
--- a/src/ui.jl
+++ b/src/ui.jl
@@ -2,13 +2,14 @@ using REPL.TerminalMenus
 import REPL.TerminalMenus: request
 using FoldingTrees
 
-mutable struct CthulhuMenu <: TerminalMenus.AbstractMenu
+mutable struct CthulhuMenu <: TerminalMenus.ConfiguredMenu{TerminalMenus.Config}
     options::Vector{String}
     pagesize::Int
     pageoffset::Int
     selected::Int
     toggle::Union{Nothing, Symbol}
     sub_menu::Bool
+    config::TerminalMenus.Config
 end
 
 function show_as_line(el, optimize::Bool)
@@ -22,7 +23,7 @@ function show_as_line(el, optimize::Bool)
 end
 
 
-function CthulhuMenu(callsites, optimize::Bool; pagesize::Int=10, sub_menu = false)
+function CthulhuMenu(callsites, optimize::Bool; pagesize::Int=10, sub_menu = false, kwargs...)
     options = vcat(map(site->show_as_line(site, optimize), callsites), ["↩"])
     length(options) < 1 && error("CthulhuMenu must have at least one option")
 
@@ -36,7 +37,8 @@ function CthulhuMenu(callsites, optimize::Bool; pagesize::Int=10, sub_menu = fal
     pageoffset = 0
     selected = -1 # none
 
-    CthulhuMenu(options, pagesize, pageoffset, selected, nothing, sub_menu)
+    config = TerminalMenus.Config(; kwargs...)
+    CthulhuMenu(options, pagesize, pageoffset, selected, nothing, sub_menu, config)
 end
 
 TerminalMenus.options(m::CthulhuMenu) = m.options
@@ -106,41 +108,7 @@ function TerminalMenus.pick(menu::CthulhuMenu, cursor::Int)
     return true #break out of the menu
 end
 
-function TerminalMenus.writeLine(buf::IOBuffer, menu::CthulhuMenu, idx::Int, cursor::Bool)
-    cursor_len = length(TerminalMenus.CONFIG[:cursor])
-    # print a ">" on the selected entry
-    cursor ? print(buf, TerminalMenus.CONFIG[:cursor]) : print(buf, repeat(" ", cursor_len))
-    print(buf, " ") # Space between cursor and text
-
+function TerminalMenus.writeline(buf::IOBuffer, menu::CthulhuMenu, idx::Int, iscursor::Bool)
     line = replace(menu.options[idx], "\n" => "\\n")
-
     print(buf, line)
 end
-
-# This used to be the original method, the above is for compatibility with Base.REPL
-function TerminalMenus.writeLine(buf::IOBuffer, menu::CthulhuMenu, idx::Int, cursor::Bool, term_width::Int)
-    cursor_len = length(TerminalMenus.CONFIG[:cursor])
-    # print a ">" on the selected entry
-    cursor ? print(buf, TerminalMenus.CONFIG[:cursor]) : print(buf, repeat(" ", cursor_len))
-    print(buf, " ") # Space between cursor and text
-
-    line = replace(menu.options[idx], "\n" => "\\n")
-    line = TerminalMenus.trimWidth(line, term_width, !cursor, cursor_len)
-
-    print(buf, line)
-end
-
-function choose_method_instance(@nospecialize(sig); params=current_params())
-    ci = callinfo(sig, Any, 1, params=params)
-    ci isa MICallInfo && return get_mi(ci)
-    ci isa Union{GeneratedCallInfo, FailedCallInfo} && return nothing
-    if ci isa MultiCallInfo
-        options = string.(ci.callinfos)
-        menu = RadioMenu(options; charset=:unicode)
-        chosen = request("Choose call to analyze:", menu)
-        if chosen != -1
-            return get_mi(ci.callinfos[chosen])
-        end
-    end
-    error("unexpected callinfo ", ci)
-end


Test patch:
diff --git a/test/runtests.jl b/test/runtests.jl
index 39ca4b8..4b74489 100644
--- a/test/runtests.jl
+++ b/test/runtests.jl
@@ -62,6 +62,15 @@ isordered(::Type{T}) where {T<:AbstractDict} = false
     callsites = find_callsites_by_ftt(empty_func, Tuple{Bool}; optimize=true)
     @test isempty(callsites)
 
+    # handle pure
+    callsites = find_callsites_by_ftt(iterate, Tuple{SVector{3,Int}, Tuple{SOneTo{3}}}; optimize=false)
+    @test occursin("< pure >", string(callsites[1]))
+    @test occursin(r"< constprop > getindex\(::.*Const.*,::.*Const\(1\)\)::.*Const\(1\)", string(callsites[2]))
+    callsites = @eval find_callsites_by_ftt(; optimize=false) do
+        length($(QuoteNode(Core.svec(0,1,2))))
+    end
+    @test occursin("< pure >", string(callsites[1]))
+
     # Some weird methods get inferred
     callsites = find_callsites_by_ftt(iterate, (Base.IdSet{Any}, Union{}); optimize=false)
     @test callsites[1].info isa Cthulhu.ConstPropCallInfo
@@ -147,6 +156,9 @@ let callsites = find_callsites_by_ftt(f_matches, Tuple{Any, Any}; optimize=false
     @test length(callsites) == 1
     callinfo = callsites[1].info
     @test callinfo isa Cthulhu.MultiCallInfo
+    io = IOBuffer()
+    Cthulhu.show_callinfo(io, callinfo)
+    @test occursin(r"#g_matches\(::Any, ?::Any\)::Union{Float64, ?Int\d+}", String(take!(io)))
 end
 
 @testset "wrapped callinfo" begin
@@ -165,7 +177,9 @@ end
         ci = first(callsites).info
         @test isa(ci, Cthulhu.UncachedCallInfo)
         @test Cthulhu.is_callsite(ci, ci.wrapped.mi)
-
+        io = IOBuffer()
+        show(io, first(callsites))
+        @test occursin("< uncached >", String(take!(io)))
         # TODO do some test with `LimitedCallInfo`, but they happen at deeper callsites
     end
 end
@@ -192,6 +206,12 @@ end
         callinfos = callinfo.callinfos
         @test length(callinfos) == 2
         @test all(ci->isa(ci, Cthulhu.ConstPropCallInfo), callinfos)
+        io = IOBuffer()
+        Cthulhu.show_callinfo(io, callinfos[1])
+        @test startswith(String(take!(io)), "getproperty")
+        io = IOBuffer()
+        print(io, callsites[1])
+        @test occursin("= call #getproperty", String(take!(io)))
     end
 
     # successful and unsuccessful constant prop'
@@ -215,6 +235,20 @@ end
         @test count(ci->isa(ci, Cthulhu.MICallInfo), callinfos) == 1
         @test count(ci->isa(ci, Cthulhu.ConstPropCallInfo), callinfos) == 1
     end
+
+    callsites = (@eval Module() begin
+            struct F32
+                val::Float32
+                _v::Int
+            end
+
+            $find_callsites_by_ftt((F32,); optimize = false) do f
+                f.val
+            end
+        end)
+    io = IOBuffer()
+    print(io, callsites[1])
+    @test occursin("= < constprop > getproperty(", String(take!(io)))
 end
 
 # Failed return_type
@@ -226,6 +260,12 @@ let callsites = find_callsites_by_ftt(return_type_failure, Tuple{Float64}, optim
     @test callinfo isa Cthulhu.ReturnTypeCallInfo
     callinfo = callinfo.called_mi
     @test callinfo isa Cthulhu.MultiCallInfo
+    io = IOBuffer()
+    Cthulhu.show_callinfo(io, callinfo)
+    @test String(take!(io)) == "#return_type(::typeof(only_ints),::Type{Tuple{Float64}})::Core.Const(Union{})"
+    io = IOBuffer()
+    print(io, callsites[1])
+    @test occursin("return_type < #return_type", String(take!(io)))
     @test length(callinfo.callinfos) == 0
 end
 
@@ -235,6 +275,9 @@ let callsites = find_callsites_by_ftt(ftask, Tuple{})
     task_callsites = filter(c->c.info isa Cthulhu.TaskCallInfo, callsites)
     @test !isempty(task_callsites)
     @test filter(c -> c.info.ci isa Cthulhu.FailedCallInfo, task_callsites) == []
+    io = IOBuffer()
+    show(io, first(task_callsites))
+    @test occursin("= task < #", String(take!(io)))
 end
 
 @testset "invoke" begin
@@ -244,6 +287,7 @@ end
         f(a::Int) = :Int
     end
 
+    io = IOBuffer()
     let
         callsites = @eval m begin
             $find_callsites_by_ftt(; optimize=false) do
@@ -252,9 +296,11 @@ end
         end
         @test any(callsites) do callsite
             info = callsite.info
+            isa(info, Cthulhu.InvokeCallInfo) && print(io, callsite)
             isa(info, Cthulhu.InvokeCallInfo) && info.ci.rt === Core.Compiler.Const(:Integer)
         end
     end
+    @test occursin("= invoke < f(::Int", String(take!(io)))
 
     let
         callsites = @eval m begin
@@ -309,7 +355,31 @@ let callsites = find_callsites_by_ftt(toggler, Tuple{Bool})
     end
 end
 
-@testset "Varargs" begin
+@testset "Varargs and printing" begin
+    @test Cthulhu.headstring(Float32) == "Float32"
+    @test Cthulhu.headstring(Vararg{Float32}) == "Float32"
+    @test Cthulhu.headstring(Union{Char,Float32}) == "Union{Char, Float32}"
+    @test Cthulhu.headstring(Union{}) == "Union{}"
+    @test Cthulhu.headstring(Vector{Int}) == "Array"
+    @test Cthulhu.headstring(Vector{T} where T) == "Array"
+
+    function checklim(f, n, strchar::Union{AbstractString,AbstractChar})
+        io = IOBuffer()
+        iolim = Cthulhu.TextWidthLimiter(io, n)
+        print(iolim, strchar)
+        return f(String(take!(iolim)))
+    end
+    @test checklim(str -> str == "α", 3, 'α')
+    @test checklim(str -> str == "", 1, 'a')
+    @test checklim(str ->  occursin("…", str), 80, "abcd"^21)
+    @test checklim(str -> !occursin("…", str), 80, "abcd"^19)
+
+    function checklim(f, n, info::Cthulhu.CallInfo)
+        io = IOBuffer()
+        iolim = Cthulhu.TextWidthLimiter(io, n)
+        Cthulhu.show_callinfo(iolim, info)
+        return f(String(take!(iolim)))
+    end
     function fsplat(::Type{Int}, a...)
         z = zero(Int)
         for v in a
@@ -319,8 +389,6 @@ end
     end
     gsplat1(T::Type, a...) = fsplat(T, a...)   # does not force specialization
     hsplat1(A) = gsplat1(eltype(A), A...)
-    io = IOBuffer()
-    iolim = Cthulhu.TextWidthLimiter(io, 80)
     for (Atype, haslen) in ((Tuple{Tuple{Int,Int,Int}}, true),
                             (Tuple{Vector{Int}}, false),
                             (Tuple{SVector{3,Int}}, true))
@@ -330,10 +398,43 @@ end
             @test cs isa Cthulhu.Callsite
             mi = cs.info.mi
             @test mi.specTypes.parameters[end] === (haslen ? Int : Vararg{Int})
-            Cthulhu.show_callinfo(iolim, cs.info)
-            @test occursin("…", String(take!(io))) != haslen
+            @test checklim(str -> occursin("...", str) != haslen, 80, cs.info)
         end
     end
+    callsites = find_callsites_by_ftt(hsplat1, Tuple{NTuple{10,Int}}; optimize=false)
+    cs = callsites[end]
+    @test checklim(str -> occursin("gsplat1(…,…,…,…,…,…,…,…,…,…,…)::Int", str), 80, cs.info)
+    callsites = find_callsites_by_ftt(hsplat1, Tuple{NTuple{50,Int}}; optimize=false)
+    cs = callsites[end]
+    @test checklim(str -> occursin("gsplat1(…)::Int", str), 80, cs.info)
+
+    # foo(x::Vector{Vector{Vector{Vector{Char}}}}) = -1
+    foo(x::Vector) = -1
+    bar() = foo([[[['c']]]])
+    callsites = find_callsites_by_ftt(bar, Tuple{}; optimize=false)
+    cs = callsites[end]
+    checklim(80, cs.info) do str
+        @test !occursin("Array{…}", str)
+        @test occursin("::Core.Const(-1)", str)
+    end
+    checklim(55, cs.info) do str
+        @test !occursin("Array{…}", str)
+        @test !occursin("::Core.Const(-1)", str)
+    end
+    checklim(40, cs.info) do str
+        @test occursin("Array{…}", str)
+        @test occursin("::Core.Const(-1)", str)
+    end
+    checklim(25, cs.info) do str
+        @test occursin("Array{…}", str)
+        @test !occursin("::Core.Const(-1)", str)
+    end
+    checklim(8, cs.info) do str
+        @test str == "foo(…)"
+    end
+    checklim(4, cs.info) do str
+        @test str == "…"
+    end
 end
 
 @testset "MaybeUndef" begin
@@ -402,12 +503,28 @@ fst5(x) = fst4(x)
     root = Cthulhu.treelist(mi)
     @test occursin("Vararg", root.data.callstr)
 
+    # Test highlighting and other printing
+    mi = Cthulhu.get_specialization(:, Tuple{T, T} where T<:Integer)
+    root = Cthulhu.treelist(mi)
+    @test occursin("\e[31m::T\e[39m", root.data.callstr)
+    mi = Cthulhu.get_specialization(Vector{Int}, Tuple{typeof(undef), Int})
+    io = IOBuffer()
+    @test Cthulhu.callstring(io, mi) == "Vector{$Int}(::UndefInitializer, ::$Int)"
+    mi = Cthulhu.get_specialization(similar, Tuple{Type{Vector{T}}, Dims{1}} where T)
+    @test occursin(r"31m::Type", Cthulhu.callstring(io, mi))
+
     # treelist for stacktraces
     tree = Cthulhu.treelist(fst5(1.0))
     @test match(r"fst1 at .*:\d+ => fst2 at .*:\d+ => fst3\(::Float64\) at .*:\d+", tree.data.callstr) !== nothing
     @test length(tree.children) == 1
     child = tree.children[1]
     @test match(r" fst4 at .*:\d+ => fst5\(::Float64\) at .*:\d+", child.data.callstr) !== nothing
+
+    # issue #184
+    tree = Cthulhu.treelist(similar(fst5(1.0), 0))
+    @test isempty(tree.data.callstr)
+    @test isempty(Cthulhu.callstring(io, similar(stacktrace(fst5(1.0)), 0)))
+    @test Cthulhu.instance(similar(stacktrace(fst5(1.0)), 0)) === Core.Compiler.Timings.ROOTmi
 end
 
 @testset "ascend" begin