Extensions, world age and generated function

Hello everyone,

I’m writing a package with extensions and ran into a world age issue. The problem is the same as in JuliaLang/julia#19942 and related to this discourse thread.

Minimal example

function bar end

@generated function foo(::Val{x}) where x
    u = bar(x)
    return :($u)
end

bar(x::Integer) = x + 1

Let say foo is defined in the main package, and bar(x::Integer) is defined in an extension triggered by a dependency. This means that when a user does:

using MainPackage
using PackageThatTriggersExtension

the world age problem occurs at load time: the @generated body calls bar at specialization time, but the extension’s method is “too new.”

  1. Is there a way to “finalize” extensions — i.e. run some code in the main package (that is the same for all extensions) each time an extension is loaded? In this case, that hook could force re-specialization of foo.

  2. Alternatively, is there a way to disable precompilation of a specific @generated function, so that it specializes lazily at first call (by which time all extensions are loaded)?

For now, my workaround is to define bar(::SomeTypeRelatedToExt, x::Integer) in each extension. There are other workarounds too, but I’d like to know if there’s a clean way to do this in Julia.

It would be in principle legal to run bar in the world age of the caller, as long as the generated functions sets the appropriate edges and bounds. That said, there isn’t a lot of supporting infrastructure to make writing that code easy. That said, in such a case, it would also be equivalent to use assume_effects (if necessary, it is not in your example) on bar to allow the compiler to perform this memoization automatically and that solution would be preferred.

Thanks! Unfortunately @assume_effects :foldable/:total didn’t work in my case (device query + string pattern matching).

I ended up with this self-memoizing pattern:

function foo(::Val{x}) where x
    result = bar(x)
    @eval foo(::Val{$x}) = $result
    return Base.invokelatest(foo, Val(x))
end

Now I do not get precompilation problem anymore since it is @eval’ed at first call…
Is this considered clean Julia? Or can I get some performance problem for some obscure reason

This problem doesn’t require precompilation or foo being specialized before defining bar(x::Integer) (method definitions alone can’t be specialized, so there isn’t an obvious place it happens in your minimal example, either); it happens if I paste it in the REPL, where foo is only called at the end and specialized for the first time:

julia> function bar end
bar (generic function with 0 methods)

julia> @generated function foo(::Val{x}) where x
           u = bar(x)
           return :($u)
       end
foo (generic function with 1 method)

julia> bar(x::Integer) = x + 1
bar (generic function with 1 method)

julia> foo(Val(1))
ERROR: MethodError: no method matching bar(::Int64)
The applicable method may be too new: running in world age 38708, while current world is 38709.

Generated functions are fundamentally stuck in the world age present during their method definition, as documented on the Manual page Metaprogramming>Generated functions:

When defining generated functions, there are five main differences to ordinary functions:

4. Generated functions are only permitted to call functions that were defined before the definition of the generated function. (Failure to follow this may result in getting MethodErrors referring to functions from a future world-age.)

Effectively, generated functions don’t experience method invalidation like normal functions (introduced by fixing Issue #265). Your example put into the REPL is nearly identical to issue Issue #23223, just uses ::Type{T} instead of ::Val{x}. It’s pointed out there that:

Method source code (“ages”) is not allowed to depend on data. This is fundamental to the design of the fix for #265. I realize that it’s weird to think of the execution of code as data, but welcome to LISP :slight_smile:

It’s just important to realize that they are part of the compiler (and thus are data), not the runtime (and thus are not similar to methods). As such, it is strictly impossible (by definition) for them to be more capable than a normal function. They are, however, capable of implementing optimizations that are not currently part of the compiler, or that aren’t worthwhile to code for generically. It would be possible to define them to be part of the runtime, but then it would be invalid for the compiler to inspect them, making there existence as an annotation pointless (because they would be strictly inferior to not annotating them at all).

I don’t know how the line between compiler+data and runtime+methods are being drawn (for instance, constants being introduced to world age in v1.12 means that I could colloquially make global data that methods depend on and are invalidated by), but it’s a deep enough characteristic that users can’t do anything.

It’s not clear what you want to accomplish. Precompilation is intended to cache things we can use long-term, but you want it to depend on optional extensions, the combination of which will vary in routine usage.

Regardless of generated functions, extensions are intended to produce methods that mix functions and types from all trigger packages, not to optionally load methods that invalidate existing behavior in just one function. That can be accomplished already with a package that directly commits type piracy (say PackageThatTriggersExtension defines bar(x::Integer)), and that’s known bad practice.

Try adding :effect_free.

Unfortunately none of the @assume_effects annotations (including :total and :effect_free) worked in my case since bar involves too complex stuff so that, I guess, the compiler cannot proofs it is ok. I ended up going with @eval to rewrite the method on first call, and warming up the cache in each backend extension’s __init__. Works perfectly even in the test environment with no warnings.