SkillAgentSearch skills...

Cthulhu.jl

The slow descent into madness

Install / Use

/learn @JuliaDebug/Cthulhu.jl
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

Cthulhu.jl CI

The slow descent into madness

[!warning] This package relies on internal APIs of the Julia base compiler. As such, its behavior may change depending on your Julia version. For maintenance reasons, the latest version of Cthulhu (typically managed in the master branch) does not guarantee compatibility with older Julia versions. Versions of Cthulhu compatible with earlier Julia releases may be maintained in dedicated branches[^old-version-branch-ex].

Please refer to the Project.toml file for details on supported Julia versions. Generally, Julia's package manager automatically selects a compatible Cthulhu version for your Julia installation. If you simply want to use Cthulhu, running pkg> add Cthulhu will install an appropriate version.

Note for nightly users: If you're using the latest Julia nightly build, we recommend regularly checking that your Cthulhu installation is up-to-date (pkg> update Cthulhu) to have compatibility with recent internal changes.

[^old-version-branch-ex]: For example, Cthulhu code compatible with Julia v1.10 and v1.11 is maintained in the 2.16 branch.

Cthulhu can help you debug type inference issues by recursively showing the type-inferred code until you find the exact point where inference gave up, messed up, or did something unexpected. Using the Cthulhu interface, you can debug type inference problems faster.

Cthulhu's main tool, descend, can be invoked like this:

descend(f, tt)     # function `f` and Tuple `tt` of argument types
@descend f(args)   # normal call

descend allows you to interactively explore the type-annotated source code by descending into the callees of f. Press enter to select a call to descend into, select ↩ to ascend, and press q or control-c to quit. You can also toggle various aspect of the view, for example to suppress "type-stable" (concretely inferred) annotations or view non-concrete types in red. Currently-active options are highlighted with color; press the corresponding key to toggle these options. Below we walk through a simple example of these interactive features; you can also see Cthulhu v2.8 in action in this video.

Usage: descend

function foo(x)
    T = rand() > 0.5 ? Int64 : Float64
    x + sum(rand(T, 100))
end

# option 1: use the function form to specify the signature
descend(foo, (Int,))
descend(foo, Tuple{Type{Int}})
descend(Tuple{typeof(foo), Type{Int}})
# option 2: use `@descend` with valid syntax to automatically extract the signature
@descend foo(1)
@descend foo(::Int) # 1.13+ only

If you do this, you'll see quite a bit of text output. Let's break it down and see it section-by-section. At the top, you may see something like this:

source-section-all

This shows your original source code (together with line numbers, which here were in the REPL). The cyan annotations are the types of the variables: Union{Float64, Int64} means "either a Float64 or an Int64". Small concrete unions (where all the possibilities are known exactly) are generally are not a problem for type inference, unless there are so many that Julia stops trying to work out all the different combinations (see this blog post for more information).

Note: if the function has default positional or keyword arguments, you may see only the signature of the function. Internally, Julia creates additional methods to fill in default arguments, which in turn call the "body" method that appears in the source text. If you're descending into one of these "default-filling" functions, you won't be able to see types on variables that appear in the body method, so to reduce confusing the entire body is eliminated. You'll have an opportunity to descend further into the "body" method in the "call menu" described below.

In the next section you may see something like

toggles

This section shows you some interactive options you have for controlling the display. Normal text inside [] generally indicates "off", and color is used for "on" or specific options. For example, if you hit w to turn on warnings, now you should see something like this:

warn

Note that the w in the [w]arn toggle is now shown in cyan, indicating that it is "on." Now you can see small concrete unions in yellow, and concretely inferred code in cyan. Serious forms of poor inferrability are colored in red (of which there are none in this example); these generally hurt runtime performance and may make compiled code more vulnerable to being invalidated.

In the final section, you see:

calls

This is a menu of calls that you can further descend into. Move the dot with the up and down arrow keys, and hit Enter to descend into a particular call. Note that the naming of calls can sometimes vary from what you see in the source-text; for example, if you're descending into kwarg-function foo, then the "body" function might be called something like #foo#123.

Any calls that are made at runtime (dynamic dispatch) cannot be descended into; if you select one, you'll see

[ Info: This is a runtime call. You cannot descend into it.

and the call menu will be printed again.

Calls that start with %nn = ... are in Julia's internal Abstract Syntax Tree (AST) form; for these calls, Cthulhu and/or TypedSyntax (a sub-package living inside the Cthulhu repository) failed to "map" the call back to the original source code.

Caveats

As a word of warning, mapping type inference results back to the source is hard, and there may be errors or omissions in this mapping. See the TypedSyntax README for further details about the challenges. When you think there are reasons to doubt what you're seeing, a reliable but harder-to-interpret strategy is to directly view the [T]yped code rather than the [S]ource code.

For problems you encounter, please consider filing issues for (and/or making pull requests to fix) any failures you observe. See CONTRIBUTING.md for tips on filing effective bug reports.

Methods: descend

  • @descend_code_typed
  • descend_code_typed
  • @descend_code_warntype
  • descend_code_warntype
  • @descend: Shortcut for @descend_code_typed
  • descend: Shortcut for descend_code_typed

Usage: ascend

Cthulhu also provides the "upwards-looking" ascend. While descend allows you to explore a call tree starting from the outermost caller, ascend allows you to explore a call chain or tree starting from the innermost callee. Its primary purpose is to support analysis of invalidation and inference triggers in conjunction with SnoopCompile, but you can use it as a standalone tool. There is a video using ascend to fix invalidations, where the part on ascend starts at minute 4:55.

For example, you can use it to examine all the inferred callers of a method instance:

julia> m = which(length, (Set{Symbol},))
length(s::Set) in Base at set.jl:55

julia> mi = m.specializations[1]      # or `mi = first(Base.specializations(m))` on Julia 1.10+
MethodInstance for length(::Set{Symbol})

julia> ascend(mi)
Choose a call for analysis (q to quit):
 >   length(::Set{Symbol})
       union!(::Set{Symbol}, ::Vector{Symbol})
         Set{Symbol}(::Vector{Symbol})
         intersect!(::Set{Union{Int64, Symbol}}, ::Vector{Symbol})
           _shrink(::typeof(intersect!), ::Vector{Union{Int64, Symbol}}, ::Tuple{Vector{Symbol}})
             intersect(::Vector{Union{Int64, Symbol}}, ::Vector{Symbol})
       union!(::Set{Symbol}, ::Set{Symbol})
         union!(::Set{Symbol}, ::Set{Symbol}, ::Set{Symbol})
           union(::Set{Symbol}, ::Set{Symbol})

You use the up/down arrows to navigate this menu, enter to select a call to descend into, and your space bar to toggle branch-folding. ascend(mi; pagesize=20) allows you to show up to 20 lines at once.

It also works on stacktraces. If your version of Julia stores the most recent error in the global err variable, you can use

julia> using Cthulhu

julia> sqrt(-1)
ERROR: DomainError with -1.0:
sqrt will only return a complex result if called with a complex argument. Try sqrt(Complex(x)).
Stacktrace:
 [1] throw_complex_domainerror(f::Symbol, x::Float64)
   @ Base.Math ./math.jl:33
 [2] sqrt
   @ ./math.jl:677 [inlined]
 [3] sqrt(x::Int64)
   @ Base.Math ./math.jl:1491
 [4] top-level scope
   @ REPL[1]:1

julia> ascend(err)
Choose a call for analysis (q to quit):
 >   throw_complex_domainerror(::Symbol, ::Float64) at ./math.jl:33
       sqrt(::Int64) at ./math.jl:1491

If this isn't available to you, a more "manual" approach is:

julia> bt = try
           [sqrt(x) for x in [1, -1]]
       catch
           catch_backtrace()
       end;

julia> ascend(bt)
Choose a call for analysis (q to quit):
 >   throw_complex_domainerror(::Symbol, ::Float64) at ./math.jl:33
       sqrt at ./math.jl:582 => sqrt at ./math.jl:608 => iterate at ./generator.jl:47 => collect_to! at ./array.jl:710 => collect_to_with_first!(::Vector{Float64}, ::Float64, ::Base.Generator{Vector{Int64}, typeof(sqrt)}, ::Int64) at ./array.jl:688
         collect(::Base.Generator{Vector{Int64}, typeof(sqrt)}) at ./array.jl:669
           eval(::Module, ::Any) at ./boot.jl:360
             eval_user_

Related Skills

View on GitHub
GitHub Stars702
CategoryDevelopment
Updated8d ago
Forks46

Languages

Julia

Security Score

95/100

Audited on Mar 31, 2026

No findings