DispatchDoctor.jl

repository·main·Indexed 42 days ago

https://github.com/milescranmer/dispatchdoctor.jl

A tool for enforcing type stability in Julia code. It provides the `@stable` macro to detect and error on non-concrete return types, helping developers write high-performance code. It includes features for applying stability checks to blocks and modules, an `@unstable` macro for exemptions, and configurable options for union limits and codegen levels.

Tokens
1.5K
Snippets
5
Records
11
Agent score
3%

What's inside DispatchDoctor.jl

  1. Apply @stable to blocks, modules, and submodules

    main

    The @stable macro can be applied to large scopes. It will recursively apply to:

    • Code inside begin...end blocks
    • module definitions
    • Included files via include()
    • Nested submodules

    All methods defined within these scopes will be wrapped with the type stability check.

    @stable begin
        f() = rand(Bool) ? 0 : 1.0
        
        module A
            g(; a, b) = a + b
            include("myfile.jl")
    
            module B
                using DispatchDoctor: @unstable
                @unstable h() = rand(Bool) ? 0 : 1.0
            end
        end
    end
  2. Scenarios where type stability checks are ignored

    main

    DispatchDoctor automatically skips stability checks in the following cases:

    • During precompilation.
    • In unsupported Julia versions (currently only 1.10.0 and 1.13.0 are active).
    • When loading code changes via Revise.jl.
    • Within @unstable blocks.
    • Within @generated blocks.
    • Within functions containing @nospecialize.
    • Within quote ... end or macro ... end blocks.
    • Within incompatible macros (e.g., @eval, @assume_effects, @pure).
    • For parameterized functions (e.g., MyType{T}(args...)).
    • For functions with expression-based names (e.g., (::MyType)(args...)).
    • For closures (the closure itself is not stabilized, but the outer function is).
  3. Integrate DispatchDoctor into a package

    main

    To use DispatchDoctor in a package without affecting downstream users, you can set the default mode to "disable". This makes @stable a no-op unless explicitly configured otherwise.

    Method 1: Wrap entire package

    module MyPackage
    using DispatchDoctor
    @stable default_mode="disable" begin
        # Entire package code
    end
    end

    Method 2: Use a wrapper macro

    Define a local macro to avoid repeating configuration:

    import DispatchDoctor
    macro stable(ex)
        return esc(:($(DispatchDoctor).@stable default_mode = "disable" $ex))
    end

    Method 3: Configure via Preferences.jl

    You can override settings globally or per-package using Preferences.jl. This is useful for enabling checks during tests:

    using Preferences: set_preferences!
    set_preferences!("MyPackage", "dispatch_doctor_mode" => "error")

    Note on Configuration Keys:

    • dispatch_doctor_mode (for default_mode)
    • dispatch_doctor_codegen_level (for default_codegen_level)
    • dispatch_doctor_union_limit (for default_union_limit)
    using Preferences: set_preferences!
    set_preferences!("MyPackage", "dispatch_doctor_mode" => "error")
  4. Configure @stable options

    main

    The @stable macro accepts several configuration options to control behavior:

    • default_mode::String="error": Sets the reaction to instability. Options are "error" (default), "warn" (emit warning only), or "disable" (no effect).
    • default_codegen_level::String="debug": Controls code generation. "debug" (default) generates a duplicate function for use with @code_warntype. "min" generates only a single function body to improve precompilation time.
    • default_union_limit::Int=1: The maximum number of elements in a Union to be considered stable. A value of 1 means all Union types are unstable. A value of 2 allows Union{Float32, Float64} to be considered stable.
  5. Identify and fix type instabilities

    main

    If @stable throws a TypeInstabilityError, use these tools to locate the source:

    1. @code_warntype: Built into Julia; highlights unstable variables in color.
    2. Cthulhu.jl: Provides detailed type instability reports.
    3. JET.jl: Provides advanced analysis and can descend into function calls to find the source of instability.
  6. Disable stability checks with @unstable or allow_unstable

    main

    You can opt-out of type stability checks in two ways:

    1. @unstable macro: Inverts the effect of @stable. Use this inside an @stable block to exempt specific functions or code blocks.
    2. allow_unstable() context: Disables stability errors for a specific scope. Note that this cannot be used simultaneously from two separate threads.
    @stable f(x) = x > 0 ? x : 0.0
    
    allow_unstable() do
               f(1)
           end
    @stable begin
        @unstable h() = rand(Bool) ? 0 : 1.0
        @unstable begin
            h(x::Int) = rand(Bool) ? 0 : 1.0
        end
    end
  7. Register macros with register_macro!

    main
    By default, @stable propagates macros to both the function definition and the function simulator. If you are developing a macro that is incompatible with @stable, or if you want to change how @stable interacts with your macro, use register_macro! to declare its compatibility or custom behavior.
  8. Use @stable and @unstable macros

    main

    DispatchDoctor provides two primary macros to manage stability annotations in your code:

    • @stable: Marks a function or code block as stable. When used, @stable propagates macros to both the function definition and the function simulator.
    • @unstable: Inverts the behavior of @stable. Use this to mark code as unstable or to override a stable context.
  9. Use the @stable macro to enforce type stability

    main

    The @stable macro is used to ensure that functions return concrete types. If a function call results in a non-concrete type (like a Union), DispatchDoctor will throw a TypeInstabilityError.

    When code is type-stable, the check is designed to compile away, resulting in zero runtime overhead in many cases.

    using DispatchDoctor: @stable
    
    @stable function relu(x)
        if x > 0
            return x
        else
            return 0.0
        end
    end