What is TypedSyntax
masterJuliaSyntax.jl and extends syntax trees with type information.repository·master·Indexed 20 days ago
https://github.com/juliadebug/cthulhu.jlA 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.
JuliaSyntax.jl and extends syntax trees with type information.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).When printing with printstyled in a REPL, the package uses colors to highlight potential type instability:
Any, Real).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)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:
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)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 CthulhuWhile 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:
descend into.=> 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)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)Users should be aware of two primary limitations:
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.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)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))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)