# Cthulhu.jl: stepping into Julia's type inference one call at a time

> Cthulhu.jl is a Julia package that walks into a function's inferred code interactively, letting you descend into callees and ascend back out until you find the point where inference gave up. It relies on internal compiler APIs, so its behaviour is tied to the Julia version you run.

**JuliaDebug/Cthulhu.jl** — The slow descent into madness

- Repository: https://github.com/JuliaDebug/Cthulhu.jl
- Stars: 711 · Forks: 46
- Language: Julia
- License: MIT
- Published: 2026-10-21 · Updated: 2026-10-21 · Language: en
- Canonical page: https://hysenlabs.com/projects/juliadebug-cthulhu-jl

## For when the profiler says inference, not the algorithm

Cthulhu.jl is a Julia package built for one narrow class of bug: code where the compiler's type inference produces something other than what you assumed. The description in the repository is a literary reference, and the package name follows it, but the job is practical. When a function in your code is unexpectedly slow, and the reason is that inference produced a Union where you expected a concrete type, you need to find the exact line where that happened.

The usual tools for this problem show you a result. Cthulhu shows you a path. It renders your source code with the inferred types annotated in place, then offers you a menu of the calls that function makes so you can step into one and see its inference too, repeating until you reach the point where things went wrong. The project puts it plainly: it recursively shows the type-inferred code until you find the exact point where inference gave up, messed up, or did something unexpected.

The audience is Julia developers who already know what type inference is and who are past the stage of asking why a whole function is slow. If you have not looked at inferred code before, the annotation syntax will need a minute of orientation. If you have, this is a familiar tool with an unusually good interface.

## descend, then ascend, then descend somewhere else

The main entry point is `descend`, and there are two ways to call it. The first takes a function and a Tuple of argument types. The second is a macro, `@descend`, applied to an ordinary call, where the signature is extracted from the call itself:

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

The documentation gives a fuller picture of the options:

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

with the calls you might make against it:

```julia
descend(foo, (Int,))
descend(foo, Tuple{Type{Int}})
descend(Tuple{typeof(foo), Type{Int}})
@descend foo(1)
@descend foo(::Int) # 1.13+ only
```

Interaction is the point of the design. You press enter to select a call to descend into, use the return arrow to ascend, and press q or control-c to quit. From there you can toggle aspects of the view, for example suppressing the type-stable annotations or colouring non-concrete types in red. Active options are highlighted in colour and toggled by pressing the corresponding key. The upward-looking counterpart is `ascend`, which walks back out toward the caller.

## The three panes and what the colours are telling you

The display has three parts and each answers a different question. At the top you get your original source code with line numbers, which is where the annotations live. The cyan annotations are the types of the variables, so a Union{Float64, Int64} means either a Float64 or an Int64. Small concrete unions, where all the possibilities are known exactly, are generally not a problem for inference unless there are so many that Julia stops trying to work out the combinations.

The middle section is the toggle panel. Text inside square brackets means the option is off, and colour means on. Pressing w turns on warnings, after which the w in the warning toggle is shown in cyan, small concrete unions appear in yellow, concretely inferred code appears in cyan, and serious forms of poor inferrability appear in red. The documentation notes there are none of the serious ones in the example. The judgement it offers is that these generally hurt runtime performance and may make compiled code more vulnerable to being invalidated.

The bottom section is the call menu. A dot moves between entries with the up and down arrow keys and enter descends into the selected call. One detail to expect: the naming of calls can differ from what the source shows, so descending into a kwarg function foo can lead to a body function named something like #foo#123. Calls that start with %nn = ... are in Julia's internal Abstract Syntax Tree form, and for those, Cthulhu or the TypedSyntax sub-package failed to map the call back to source.

## What you cannot descend into, and why it matters

There is a hard stop in the interface, and it is worth understanding before you rely on the tool. Any call that is made at runtime, in other words dynamic dispatch, cannot be descended into. Selecting one gives you this:

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

and the call menu is printed again. That boundary defines the tool's reach. A dynamic call site is exactly the point where inference often gives up, so the place you most want to inspect is the place you cannot step into. You can still see that a runtime call is there in the annotated source, but the recursive walk stops at the boundary.

The second limitation is more uncomfortable. Mapping inference results back to source is described in the caveats as hard, and there may be errors or omissions in the mapping. The documentation points to a reliable but harder-to-interpret strategy for when you doubt what you are seeing: view the typed code directly rather than the source code, using the internal representation view. The same caveat is why the TypedSyntax sub-package exists inside the repository, and it is a separate README that explains the mapping challenges in detail. The practical reading is that Cthulhu's source view is a good navigation aid, not a proof.

## Internal compiler APIs make the version matrix a real constraint

The warning at the top of the project documentation is the thing to read before anything else. Cthulhu relies on internal APIs of the Julia base compiler, so its behaviour may change depending on your Julia version. The latest version, typically managed in the master branch, does not guarantee compatibility with older Julia releases, for maintenance reasons. Code compatible with Julia v1.10 and v1.11 is maintained in a separate branch, the 2.16 branch, and Project.toml holds the details on supported Julia versions.

The package manager handles this in the ordinary way, since Julia's package manager automatically selects a compatible Cthulhu version for your installation, and running pkg> add Cthulhu installs an appropriate version. If you are on the latest Julia nightly, the documentation advises checking regularly that your Cthulhu installation is up to date, with pkg> update Cthulhu, for compatibility with recent internal changes.

So the upgrade cost is real but bounded. The binding constraint is that a project pinned to an older Julia release is effectively pinned to a Cthulhu branch that receives maintenance rather than new work. For a team that upgrades Julia regularly this is a non-issue. For a team on a frozen Julia version it means your type inference debugging tool is frozen too, and you will be reading a branch that is no longer where the package is developed.

## The code_typed and code_warntype entry points underneath descend

The README lists a family of methods, and the naming is worth unpacking because it explains what descend actually is. The primitives are @descend_code_typed and descend_code_typed, along with @descend_code_warntype and descend_code_warntype. The @descend macro and the descend function are shortcuts for the code_typed versions, which means the interactive descent is built on the same machinery that produces the typed representation of a method.

This matters for the failure modes. The code_warntype variants exist in the same family, and the coloured view that descend shows, with cyan for concretely inferred code and red for serious inferrability problems, is the same kind of information that code_warntype reports as plain text. The difference is that descend adds the navigation. When you already know the method you care about and just want the annotations, the code_warntype route is the one that produces a dump you can diff and paste into an issue. When you do not know which method is at fault, descend lets you walk the call tree and find out.

The repository layout backs this up with a src/ directory, a test/ directory, an ext/ directory for extensions, and the TypedSyntax/ sub-package, plus a CHANGELOG.md and CONTRIBUTING.md at the root. The documentation asks for issues and pull requests when mapping failures are observed, and CONTRIBUTING.md is the place it points to for how to file them.

## How this compares with reading a flat inference report

The alternative is the non-interactive route. Julia ships code_warntype and code_typed, and the standard workflow for an inference problem is to call code_warntype on a function, read the annotated output in the terminal or in the browser, and reason about it by eye. That is a complete method, and for a function of moderate size it is often enough.

The difference in approach is where you start. A flat report tells you everything about one method and nothing about the path that led there, so finding the offending call inside a large function means reading the output carefully and then repeating the process on each callee you suspect. Cthulhu starts from the same information and turns it into a tree, with a menu of the calls that the current method makes, so you choose a branch and step into it. The argument is the same one that separates a debugger from a log dump.

The cost of the interactive approach is that you are reading a terminal interface rather than a static document, which makes it a poor fit for recording a result in a ticket or reviewing it with a colleague. A sensible practice is to descend to find the culprit, then reproduce the finding with the typed code view for the record, which is also what the documentation suggests when you doubt what you are seeing.

## Conclusion

Cthulhu.jl is worth adopting when you already know your slowdown is a type inference problem and you want to walk into the inferred code rather than read a flat report, and when your Julia version is recent enough to match the master branch. It is the wrong tool for a general performance mystery, for a codebase that cannot move to a current Julia release, and for anyone who needs a stable API contract, because the package sits on internal compiler APIs that can change. Before adopting, check the compat bounds in Project.toml for your Julia version, run pkg> add Cthulhu, and confirm the descend output matches what Cthulhu reports against the typed view, since the source mapping can carry errors and omissions. The last push was on 2026-08-22 and the listed releases are v2.16.7 and v2.12.7 from 2026-03-03, so expect a version bump whenever you move Julia versions.

## FAQ

### What does Cthulhu.jl do in Julia?

Cthulhu.jl is a package for debugging type inference issues. It shows the type-annotated source of a function and lets you descend into its callees interactively, or ascend back toward the caller, until you find the point where inference gave up or produced something unexpected.

### How do I install Cthulhu.jl and which Julia versions are supported?

Running pkg> add Cthulhu installs a version compatible with your Julia installation, because the package manager selects one automatically. The compat bounds are listed in Project.toml, and code for older Julia releases is kept on separate branches such as the 2.16 branch for Julia v1.10 and v1.11.

### How do I use descend in Cthulhu.jl?

You can call descend(f, tt) with a function and a Tuple of argument types, or use the @descend macro on a normal call so the signature is extracted automatically. Inside the interface, enter selects a call to descend into, the return arrow ascends, and q or control-c quits.

### Why can I not descend into a call in Cthulhu.jl?

Calls made at runtime, meaning dynamic dispatch, cannot be descended into, and selecting one prints a runtime call notice before showing the call menu again. The documentation also warns that mapping inference results back to source is hard and may contain errors or omissions.

## Sources

- [Issues](https://github.com/JuliaDebug/Cthulhu.jl/issues)
- [JuliaDebug/Cthulhu.jl on GitHub](https://github.com/JuliaDebug/Cthulhu.jl)
- [License: MIT](https://github.com/JuliaDebug/Cthulhu.jl/blob/master/LICENSE)
- [README](https://github.com/JuliaDebug/Cthulhu.jl/blob/master/README.md)
- [Releases](https://github.com/JuliaDebug/Cthulhu.jl/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/juliadebug-cthulhu-jl
