Cthulhu.jl

repository·master·Indexed 20 days ago

https://github.com/juliadebug/cthulhu.jl

A debugging tool for Julia developers to diagnose type inference issues. It provides interactive interfaces to 'descend' into function calls to view type-annotated source code or 'ascend' from methods and errors to trace callers. The tool leverages TypedSyntax to map compiler type-inference results back to original source code, helping users identify type instability through color-coded annotations and internal representation views.

Tokens
2.6K
Snippets
9
Records
13
Agent score
22%

What's inside Cthulhu.jl

  1. What is TypedSyntax

    master
    TypedSyntax is a package designed to map types determined via Julia's type-inference back to the original source code. It allows developers to identify causes of "type instability" (inference failures) by viewing type annotations directly on the syntax tree or in a reconstructed source code format, without needing to inspect Julia's intermediate representations (AST). It is built on top of JuliaSyntax.jl and extends syntax trees with type information.
  2. View the internal representation of Julia code

    master

    While Cthulhu places type annotations directly on source code, this can sometimes obscure details. For deeper insight, you can examine Julia's internal type-inferred representations.

    Key concepts for interpreting inferred code:

    • invoke statements: Represent calls that can be statically dispatched (inferred).
    • call statements: Represent dynamic dispatch (not fully inferred).
    • Transformations: Depending on optimization levels, the representation may include inlining and other compiler transformations.
  3. Interpret color-coded type instability

    master

    When printing with printstyled in a REPL, the package uses colors to highlight potential type instability:

    • Red: Indicates non-concrete types (e.g., Any, Real).
    • Yellow: Indicates a "small union" of concrete types. These are generally safe unless the number of combinations becomes too large.
  4. Combine static and runtime information to improve type inference

    master

    Cthulhu primarily uses "static" type information available to the Julia compiler. In complex scenarios (e.g., multiple conditional branches), this can result in incomplete or misleading Union type annotations.

    To provide Cthulhu with more complete type information, you can combine it with runtime information by using a debugger like Infiltrator.jl. By infiltrating a function at a specific point, you can run @descend within the REPL scope where variables have concrete runtime types, allowing Cthulhu to show fully inferred types instead of broad Union types.

    using Infiltrator: @infiltrate
    using Cthulhu: @descend
    
    function foo(n)
        x = n < 2 ? 2 * n : 2.5 * n
        y = n < 4 ? 3 * n : 3.5 * n
        z = n < 5 ? 4 * n : 4.5 * n
        @infiltrate  # Infiltrate here to provide runtime context
        bar(x, y, z)
    end
    
    @noinline function bar(x, y, z)
        string(x + y + z)
    end
    
    # Run the function to enter the Infiltrator REPL
    foo(4)
    
    # Inside the infil> REPL, run:
    # @descend bar(x, y, z)
  5. Use `descend` to debug type inference

    master

    The descend tool allows you to recursively explore type-annotated source code to find where type inference fails or behaves unexpectedly. You can interact with the output using the following controls:

    • Enter: Select a call from the menu to descend into it.
    • ↩ (Up Arrow/Return): Ascend back up the call tree.
    • q or Ctrl-C: Quit the session.
    • Interactive Toggles: Press specific keys to toggle view options (e.g., w to toggle warnings, o to toggle between optimized/non-optimized views). Currently active options are highlighted in color.

    Caveat: Mapping type inference results back to source code is complex and may contain errors or omissions. If you suspect the source view is inaccurate, use the [T]yped code view instead.

    # Option 1: Specify the function and a Tuple of argument types
    descend(foo, (Int,))
    
    # Option 2: Use the @descend macro to automatically extract the signature
    @descend foo(1)
    
    # For Julia 1.13+
    @descend foo(::Int)
  6. Install Cthulhu.jl

    master

    To install Cthulhu.jl, use the Julia package manager. It is recommended to let the package manager select a version compatible with your Julia installation.

    Note for nightly users: If you are using a Julia nightly build, regularly run pkg> update Cthulhu to ensure compatibility with recent internal compiler changes.

    using Pkg
    Pkg.add("Cthulhu")
    # Or in the REPL:
    # pkg> add Cthulhu
  7. Use `ascend` to explore call chains

    master

    While descend starts from a caller and goes down, ascend starts from a callee and looks upwards at its callers. This is useful for analyzing invalidation triggers or tracing how a specific method instance was reached.

    Navigation in ascend:

    • Up/Down Arrows: Navigate the menu.
    • Enter: Select a call to descend into.
    • Space bar: Toggle branch-folding.
    • => separator: Indicates inlined methods. Selecting a line with => takes you to the final (topmost) call in that chain.

    Controls:

    • o: Toggle non-optimized code view.
    • w: Toggle warning coloration for types.
    • q: Quit.
    # 1. Ascend from a MethodInstance
    m = which(length, (Set{Symbol},))
    mi = m.specializations[1] # or first(Base.specializations(m)) on Julia 1.10+
    ascend(mi)
    
    # 2. Ascend from an error (if stored in `err`)
    ascend(err)
    
    # 3. Ascend from a stacktrace
    bt = try
        [sqrt(x) for x in [1, -1]]
    catch
        catch_backtrace()
    end
    ascend(bt)
  8. Customize Cthulhu configuration

    master

    You can customize the default configuration of toggles in the @descend menu using Cthulhu.CONFIG. To make these changes persistent across Julia sessions, use Cthulhu.save_config!(), which utilizes Preferences.jl.

    # Change a default setting
    Cthulhu.CONFIG.enable_highlighter = true
    
    # Persistently save the current configuration
    Cthulhu.save_config!(Cthulhu.CONFIG)
    
    # Overwrite existing preferences forcefully
    Cthulhu.save_config!(Cthulhu.CONFIG; force = true)
  9. Limitations and Caveats of TypedSyntax

    master

    Users should be aware of two primary limitations:

    1. Anonymous and Internal Functions: Types inside anonymous functions (e.g., x -> first(x)) are hidden from the annotator because Julia handles them as separate type-inferred methods. Consequently, variables inside these functions may not be annotated correctly in the parent function's tree.
    2. Mapping Failures: Due to the way Julia lowers code, some statements in the type-inferred representation may not map directly back to a specific source line (resulting in empty mappings). However, named variables usually provide enough information for successful annotation.
  10. Display annotated source code with printstyled

    master

    You can use printstyled to display the code in a format similar to the original source, but with type annotations included.

    By default, hide_type_stable=true suppresses concrete types to help you focus on type instability. To see all inferred types, set hide_type_stable=false.

    To suppress colorized output in the REPL, use the iswarn=false keyword argument.

    # Show all types (including concrete ones)
    printstyled(stdout, node; hide_type_stable=false)
    
    # Default behavior (hides stable concrete types to highlight instability)
    printstyled(stdout, node)
  11. Create a TypedSyntaxNode

    master

    To map types to source code, use the TypedSyntaxNode constructor. You must provide the function to be analyzed and a tuple representing the types of its arguments. This creates a syntax tree where each node is annotated with its inferred type.

    using TypedSyntax
    
    f(x, y, z) = x + y * z;
    
    # Create a node for function f with argument types (Float64, Int, Float32)
    node = TypedSyntaxNode(f, (Float64, Int, Float32))
  12. Analyze mapping between inferred code and source code

    master

    Because TypedSyntax works by attempting to "reconstruct history" from type-inferred code back to source, some mappings may be empty if the lowering process changes the implementation significantly.

    You can inspect these mappings using TypedSyntax.tsn_and_mappings(function, argument_types). This returns a tuple containing the TypedSyntaxNode and a collection of mappings. The mappings show how statements in the type-inferred code relate to the original source code.

    function summer(list)
       s = 0
       for x in list
           s += x
       end
       return s
    end;
    
    # Get the node and the source mappings
    tsn, mappings = TypedSyntax.tsn_and_mappings(summer, (Vector{Float64},));
    
    # View mappings: left column is inferred code, right column is source mapping
    hcat(1:length(mappings), tsn.typedsource.code, mappings)