JET.jl

repository·master·Indexed 21 days ago

https://github.com/aviatesk/jet.jl

A static analysis tool for Julia that leverages the language's type inference system to identify type instabilities and potential type-related bugs. It provides tools like `@report_call` for detecting runtime errors, `report_package` for analyzing package method definitions, and `@test_call` for integrating static analysis into unit testing workflows. JET supports different analysis modes (default, :sound, and :typo) and allows scope configuration via `ignored_modules` and `target_modules`.

Tokens
6.6K
Snippets
28
Records
33
Agent score
61%

What's inside JET.jl

  1. Install and use JET.jl

    master

    JET.jl is a tool that employs Julia's type inference system to detect potential bugs and type instabilities in your code.

    Compatibility Warning: JET is tightly integrated with the Julia compiler. Results can vary significantly depending on your Julia version and the implementation of the Base module.

    Version Support:

    • The latest release series (v0.12) supports full functionality on Julia v1.12 and v1.13 only.
    • For Julia v1.11, use the v0.9 series.
    • On unsupported Julia versions, JET may still install but will load empty stubs by default, causing analysis APIs to throw errors.
  2. Understand the difference between default and sound analysis modes

    master

    JET's error reporting behavior changes based on the analysis mode:

    • Default Mode: JET only reports exceptions that it detects will always be thrown (and are not caught). It will not report a potential exception if it is only a possibility.
    • Sound Mode (mode=:sound): JET reports any function that may throw an exception, even if it is not guaranteed to throw. This is useful for catching edge cases that might cause runtime failures.
    using JET
    # Default mode (only reports certain throws)
    @report_call my_function(arg)
    
    # Sound mode (reports potential throws)
    @report_call mode=:sound my_function(arg)
  3. Change analysis modes with `mode`

    master

    JET provides different analysis modes to balance between strictness and noise. You can switch modes using the mode keyword argument.

    • Default mode: Collects problems based on a standard definition of errors.
    • :sound mode: A stricter mode that requires more precise type requirements (e.g., requiring a conditional value to be strictly Bool rather than just something that can be used in a boolean context).
    • :typo mode: A simpler mode that only reports obvious typos (e.g., calling an undefined function).
    # Default mode
    report_call(my_func, (Int,))
    
    # Sound mode (stricter)
    report_call(my_func, (Int,); mode=:sound)
    
    # Typo mode (only reports undefined names)
    @report_call mode=:typo my_func(args...)
    # the typo detection pass will only report the "typo"
    @report_call mode=:typo strange_sum([])
  4. Understand JET's analysis limitations

    master

    JET explores functions you call directly and their inferable callees.

    Key Limitation: If the argument types for a call cannot be inferred, JET does not analyze the callee. Therefore, a result of No errors detected does not guarantee that your entire codebase is free of errors.

    Best Practice: To increase confidence in JET's results, use @report_opt first to ensure your code is inferable, which allows JET to traverse deeper into your call stack.

  5. How JET.jl performs abstract interpretation

    master

    JET.jl performs type-level program analysis by utilizing the Compiler.AbstractInterpreter interface. It implements its core functionality by overloading a subset of Compiler functions.

    JET.AbstractAnalyzer serves as the base infrastructure, overloading methods to handle interprocedural propagation of error reports and caching of analysis results. Specialized plugin analyzers (like JET.JETAnalyzer) build upon this by overloading additional Compiler methods to implement specific analysis logic.

  6. Handle JET.jl compatibility in test environments

    master

    Because JET's full functionality is only available on specific Julia versions, you should use the JET.JET_AVAILABLE constant to conditionally run JET-specific tests. This prevents your test suite from failing on unsupported Julia versions where JET only loads empty stubs.

    To force JET to attempt loading full functionality on an unsupported Julia version (for debugging or development purposes), set the JET_DEV_MODE preference to true.

    using JET
    
    if JET.JET_AVAILABLE
        include("jet_tests.jl")
    end
  7. Fix `no matching method found (x/y union split)` errors

    master

    This error occurs when a variable is inferred to be a union type (e.g., Union{Int, String}), and calling a function with one of those types would result in a MethodError.

    There are three primary ways to resolve this:

    1. Handle the edge case: Use an explicit check (like if p === nothing) to handle the possibility of the union member that causes the error. This allows the compiler to refine the type in the else block.
    2. Use a type assertion: If you know the value cannot be a certain type (e.g., nothing), use a type assertion (e.g., ::Integer). This tells the compiler to treat the value as that specific type, which also improves performance by enabling more precise type inference.
    3. Avoid repeated field loading from mutable structs: When working with Union-typed fields in mutable structs, the compiler may not realize that multiple loads of the same field return the same object. To fix this, assign the field to a local variable before performing checks or operations.
    # Option 1: Handle the nothing case
    function pos_after_tab(v::AbstractArray{UInt8})
        p = findfirst(isequal(UInt8('\t')), v)
        if p === nothing
            return nothing
        else
            return p + 1
        end
    end
    
    # Option 2: Use a type assertion
    function pos_after_tab(v::AbstractArray{UInt8})
        p = findfirst(isequal(UInt8('\t')), v))::Integer
        p + 1
    end
    
    # Option 3: Assign field to local variable for mutable structs
    function f(x)
        y = x.x
        if y === nothing
            nothing
        else
            y + 1
        end
    end
  8. Quick start with JET.jl

    master

    To begin using JET, import the package using using JET. The primary way to interactively analyze code is through @report_call (macro) or report_call (function), which perform static analysis on a specific function call to detect potential runtime errors without executing the code.

    using JET
    
    # Analyze a specific call
    @report_call sum("julia")
    using JET
    
    @report_call sum("julia")
  9. Analyze packages using a representative workload

    master

    While report_package is available, it can be imprecise due to generic type signatures. For more precise analysis, create a representative workload function (e.g., in src/workload.jl) that uses concrete types by exercising your package's functionality.

    Workflow

    1. Define a function exercise_mypkg() that calls your package functions with concrete data.
    2. Run @report_call exercise_mypkg() to get precise type-stability and error reports.

    Analyzing existing PrecompileTools workloads

    If you already have a precompilation workload, you can inspect the compiled methodinstances directly:

    using MyPkg, JET, MethodAnalysis
    
    # Get all compiled methodinstances for the package
    mis = methodinstances(MyPkg)
    
    # Filter for instances that produce JET reports (errors/instabilities)
    badmis = filter(mis) do mi
        !isempty(JET.get_reports(report_call(mi)))
    end
    
    # Inspect problematic instances
    for mi in badmis
        report_call(mi)
    end

    Caveats:

    • methodinstances(MyPkg) only covers functions owned by MyPkg. Extension methods (e.g., OtherPkg.f(...)) will be listed under OtherPkg.
    • Ensure precompilation is enabled; disabling it will result in a list of methodinstances that does not reflect real-world usage.
    using MyPkg, JET, MethodAnalysis
    
    # Get all compiled methodinstances for the package
    mis = methodinstances(MyPkg)
    
    # Filter for instances that produce JET reports
    badmis = filter(mis) do mi
        !isempty(JET.get_reports(report_call(mi)))
    end
  10. Debug inference failures with Cthulhu integration

    master

    If @report_opt identifies an issue, you can use Cthulhu.jl to interactively inspect the call tree and see exactly where type inference fails.

    1. Get the report from JET.
    2. Extract individual reports using JET.get_reports(report).
    3. Use Cthulhu.ascend(report) to navigate the call stack and view type-annotated code with red highlighting on non-inferable arguments.
    using JET
    using Cthulhu
    
    # 1. Get the report
    report = @report_opt sumup(sin)
    
    # 2. Extract individual reports
    rpts = JET.get_reports(report)
    
    # 3. Interactively ascend the call tree to find the source of the issue
    ascend(rpts[1])
  11. Analyze scripts and apps using a main function or report_file

    master

    To analyze a standalone Julia script or application, you have two primary options:

    1. Wrap logic in a main() function: This allows JET to analyze the entire script's execution flow. You can then use @report_call main().
    2. Use report_file: Call report_file("path/to/script.jl"). This is functionally equivalent to calling @report_call main() if the script invokes a main() function at the top level.
    # Option 1: Wrap in main
    function main()
        # script logic here
    end
    main()
    
    # Analyze via macro
    @report_call main()
    
    # Option 2: Use report_file
    report_file("my_script.jl")
  12. Detect performance pitfalls with `@report_opt`

    master

    Use the @report_opt macro to analyze the entire call graph of a generic function call and automatically detect performance issues such as runtime dispatch, captured variables, and unresolvable recursive calls. This is useful for identifying where Julia's type inference fails and prevents optimizations like inlining.

    To use it, first ensure JET is loaded, then wrap your target function call with the macro.

    using JET
    
    # Example: detecting runtime dispatch caused by a non-constant global
    n = rand(Int)
    make_vals(n) = n ≥ 0 ? (zero(n):n) : (n:zero(n))
    function sumup(f)
        vals = make_vals(n)
        s = zero(eltype(vals))
        for v in vals
            s += f(v)
        end
        return s
    end
    
    @report_opt sumup(sin) # This will report runtime dispatches