ERROR: Method overwriting is not permitted during Module precompilation. Use `__precompile__(false)` to opt-out of precompilation

Hello,
I switched to julia 1.10 and see that one of my packages now shows an error during precompilation

ERROR: Method overwriting is not permitted during Module precompilation. Use `__precompile__(false)` to opt-out of precompilation.

How to do this then in 1.10? I want to adapt a certain method from a certain package (GitHubHelpers) for my needs. There should be a way to compile this similar to how PrecompileTools.jl supports recompiling invalidations, shouldn’t it?

You can overwrite methods from another module within __init__ of your package, while you need to remember that this may trigger invalidations at the package loading time.

2 Likes

How can I precompile those invalidations similar to PrecompileTools.jl?

Do I need a second Package to do so? That would sound unnecesarily complex, I hope there is a simple version.

My admittedly incomplete understanding is that this is an escalated discouragement (used to be a precompilation failure and warning) of type-pirating method replacement because this otherwise allows package loading order to determine which method exists. I linked a clear example, but in short, if both packages B and C overwrite a method in a dependency A, using B, C exposes a different method from using C, B. You probably only have the 1 package overwriting a method in the dependency, but someone else might make another, and whoever tries to use both your packages will encounter this problem. This kind of type piracy was always a problem, but it’s even more so now that there is much more precompilation to waste.

Given an error instead of a warning, you can’t leave it alone at failed precompilation anymore. Method replacement in __init__ cannot be precompiled and thus dodges the issue, though it still invalidates methods in the dependency. More drastically, you could opt the entire module out of precompilation with __precompile__(false), which the upcoming precompiler fix seems to do for you. Errors that point out more things more specifically could be nice for all the things we can’t feasibly precompile like load-time constants, memory address-dependent values, or referencing other modules’ global state.

1 Like

I would be very happy to allow for package loading order to determine which method exists.

I also see that usually you like to prevent such cases, but I am in a production setup, no one is using my package probably. It is deployed as an application, but that is it. I guess here also comes the workaround: Creating a custom list of precompile statements which are then hardcoded into a sysimage using PackageCompiler.jl. Pre-julia-1.9 style of precompilation.

I read up on the issue you linked, it seems a pity to me that because of possible confusions, something totally deterministic is disallowed entirely (for precompilation of course). But ok, apparently others had massive difficulties with this.

I heard that even Base uses type-piracy somewhere… Maybe that is wrong, but if it is still true, how does Base solve it? Falling back to just in time compilation? That is unwanted definitely.

I was really hoping that with julia 1.9 PackageCompiler.jl wouldn’t be really needed any longer. Instead it seems to be the only way julia supports for precompiling type-piracy.

To defend type-piracy in case someone asks: You can fix stuff on the fly for your own product without the need of waiting for PullRequests which maybe never reach main. You can also add extra features to other packages, which you already know will never be accepted upstream. Still they are useful for you and it so awesome that in Julia you can just do it.

Please if you know about an alternative to PackageCompiler and sysimages for precompiling type piracy, let me know.

This doesn’t discourage all type piracy. For example, B defining A.foo(x::Int64)=1 and C defining A.foo(x::Int32)=2 would invalidate methods precompiled in A to use A.foo(x::Integer)=0 to adapt method dispatch to the altered method table. However, you can see that using B, C and using C, B end up with the same methods. The order only matters if the type piracy involves replacing an existing method signature (imagine both B and C defined A.foo(y::Int) instead), hence “overwriting”.

Sure, just opt out of precompiling the overwriting methods. It’s not that it’s impossible to precompile, but it was just decided that it wasn’t worth doing, even when it was just a warning. Method invalidations, especially those that hit the precompile cache, were painstakingly reduced since v1.6 to justify expanding precompilation in v1.9, so it makes sense to disallow precompilation of something designed for invalidation, especially to make package load order matter.

Also worth pointing out that this pushes people in the direction of editing the dependency instead of piracy overwriting. Making the change at the origin in your environment allows precompilation and is more reproducible. Let’s for the moment assume precompilation is disabled. We can conditionally define methods in the global scope, like depending on the value of A.foo(1). When we later overwrite the method to change that value, those conditional definitions don’t get a do-over. If we allow B and C to conditionally define methods based on the existing A.foo before their own overwriting, the dependency on load order infects different functions. I’ve been talking a lot about type piracy because package interactions are the big reason, but method overwriting breaks precompilation even within 1 package. Technically changing dispatch like non-overwriting type piracy would also do this, but overwriting seems singled out as egregious.

All that said, the best practice is to let modules be modules, settling the necessary method dispatches before the conditional definitions and not interfering in each other’s dispatches, only extending method tables with isolated branches to reach the same tree in any order.

2 Likes

Great explanations thanks!

In this case, how could I resolve Warnings for : Relevancestacktrace.jl

[ Info: Precompiling RelevanceStacktrace [6c8a4c8a-cd07-4735-95b7-b1ced2eaf8fd]
WARNING: Method definition show_full_backtrace(IO, Array{T, 1} where T) in module Base at errorshow.jl:598 overwritten in module RelevanceStacktrace at /home/master/repo/julia-awesomeness/RelevanceStacktrace.jl/src/RelevanceStacktrace.jl:151.
ERROR: Method overwriting is not permitted during Module precompilation. Use `__precompile__(false)` to opt-out of precompilation.
[ Info: Skipping precompilation since __precompile__(false). Importing RelevanceStacktrace [6c8a4c8a-cd07-4735-95b7-b1ced2eaf8fd].

What would you suggest then in this case?

If I put it in the init()

__init__() = begin 
  Base.show_full_backtrace(io::IO, trace::Vector; print_linebreaks::Bool) = show_full_backtrace_relevance(io, trace, print_linebreaks)
end

then I got another problem:

[ Info: Precompiling RelevanceStacktrace [6c8a4c8a-cd07-4735-95b7-b1ced2eaf8fd]
ERROR: LoadError: syntax: Global method definition around /home/master/repo/julia-awesomeness/RelevanceStacktrace.jl/src/RelevanceStacktrace.jl:153 needs to be placed at the top level, or use "eval".
.......

eval for me really discouraged, then even having the warnings sounds better I guess.

__precompile__(false) also an option but also it isn’t nice as it can cause a lot of precompilation issue by time for other packages.

What to do when we really wants to overload another package’s function and wants to take the invalidations for that action if it cannot be handled better?

1 Like

I run into the similar problem that even if using @eval this will fail when importing the module in another module Using `eval` into the current module/package in `__init__` causes precompilation error when the package is `import`-ed in another package · Issue #39648 · JuliaLang/julia · GitHub

so really the only option is to use __precompile__(false) which is really tough as also all packages depending on this won’t be precompiled if I remember correctly.

TLDR: There is no easy way any longer to patch other packages adhoc.

Overwriting a method from another module inside __init__ looks impossible. How to do that?