Recompile generated function when methods get defined

Normally, generated functions can only use methods defined earlier:

julia> f(x) = 1
julia> @generated g(x...) = f(x...)
julia> f(x, y) = 2

julia> g(1)
1
julia> g(1, 1)
ERROR: MethodError: no method matching f(::Type{Int64}, ::Type{Int64})
The applicable method may be too new: running in world age 33465, while current world is 33466.

How can I make Julia recompile g whenever f changes (new f methods get defined)? That’s definitely possible, Tricks.jl does this stuff, but I couldn’t really understand nor utilize code from that package so that g works in the above example.

Can you please advice on how to achieve that? I guess some compilation backedges need to be inserted, but couldn’t adapt Tricks code for such a case myself.

1 Like

It’s compilicated. You basically need to turn the generated expression into a CodeInfo, and then attach backedges manually to that codeinfo

Btw, is there a reason this behavior isn’t the default? Regular functions get invalidated when relevant methods appear, why not do the same for generated?

Here’s an example of doing it to your g function. It should work on current versions of julia, but would need some tweaks to work with the current master due to a change in how generated functions interact with some internals

using Core: SimpleVector, svec, CodeInfo
using Core.Compiler: MethodInstance, method_instances

function expr_to_codeinfo(m::Module, argnames, spnames, sp, e::Expr)
    lam = Expr(:lambda, argnames,
               Expr(Symbol("scope-block"),
                    Expr(:block,
                         Expr(:return,
                              Expr(:block,
                                   e,
                                   )))))
    ex = if spnames === nothing || isempty(spnames)
        lam
    else
        Expr(Symbol("with-static-parameters"), lam, spnames...)
    end

    # Get the code-info for the generatorbody in order to use it for generating a dummy
    # code info object.
    ci = ccall(:jl_expand_and_resolve, Any, (Any, Any, SimpleVector), ex, m, svec(sp...))
    @assert ci isa CodeInfo "Failed to create a CodeInfo from the given expression. This might mean it contains a closure or comprehension?\n Offending expression: $e"
    ci
end
@generated function g(args...) 
    ex = :(f($(args...)))
    ci = expr_to_codeinfo(@__MODULE__(), [Symbol("#self#"), (Symbol(:arg, i) for i ∈ 1:length(args))...], [], (), ex)
    ci.edges = MethodInstance[]
    for mi ∈ method_instances(f, Tuple{args...})
        push!(ci.edges, mi)
    end
    return ci
end

and then we see it properly recompile as we add methods

julia> f(x) = 1
f (generic function with 1 method)

julia> g(1)
1

julia> f(x, y) = 2
f (generic function with 2 methods)

julia> g(1, 1)
2
2 Likes

Thanks for a nice easy-to-use recipe! However, this isn’t equivalent to my g function: mine executes f compiletime, yours puts its call into runtime.
Difference clearly visible with

julia> f(x) = :(1+1)
julia> f(x, y) = :(2+2)

Replacing ex = :(f($(args...))) with ex = f(args...) in your example doesn’t help: MethodErrors remain.

Oh, you want to recompile the generator, yeah I missed that. Unfortunately, due to a limitation I don’t quite understand, the backedge insertion thing only works in the function actually appears in the body, it won’t work if you call the function at comptime.

I think this issue is related: Generator output with explicit edges not always invalidated · Issue #34962 · JuliaLang/julia · GitHub

Doesn’t Tricks recompile the generator somehow? Eg, its hasmethod changes from compile-time false to compile-time true when a method is defined. So, seems possible…