Inspired by LazyModules I’m trying to make delayed loading of some of submodules from my package. However, it implies that at some point I need to update a module definition on the fly. It works well with normal functions, but by some reason using keyword arguments breaks Base.invokelatest
.
Below is a simplified example (still long, sorry, but nothing shorter reproduces the desired behavior).
module T
# Code for lazy submodules
mutable struct LazySubmodule
_module_name::Symbol
_code::Expr
_module::Union{Module, Nothing}
end
macro lm(ex)
alias, code = ex.args
s = LazySubmodule(alias, code, __module__)
Core.eval(__module__, :($(alias) = $s))
end
function Base.getproperty(m::LazySubmodule, s::Symbol)
if s in (:_module_name, :_code, :_module)
return getfield(m, s)
end
# Compile the sub-module
Core.eval(m._module, :($(m._code)))
alias = m._code.args[2]
# Rewrite it's alias with the compiled module instead of the LazySubmodule
mod = Core.eval(m._module, :($(m._module_name) = $(alias)))
f = getfield(mod, s)
# invokelatest should solve the issue with world age and missing functions
return (args...; kwargs...) -> Base.invokelatest(f, args...; kwargs...)
end
# Define my submodules
@lm M1 = module Module1
f1(s::String) = print(s)
end
f(s::String) = M1.f1(s)
end
This code doesn’t have any keyword arguments, and if I call T.f("Hello")
, it prints “Hello” as expected. However, if I add keyword arguments to the module definitions as below, the code breaks.
@lm M1 = module Module1
f1(;s::String) = print(s)
end
f(s::String) = M1.f1(s=s)
Now, the first call T.f("Hello")
fails with error
ERROR: MethodError: no method matching f1(; s="Hello")
Closest candidates are:
f1(; s)
while the second call works fine. It looks exactly like world age problems, and if I remove invokelatest
, the same behavior could be observed for the case 1 (no keyword arguments), as well.
Could anyone please explain why this is different with and without kwargs, and how it could be fixed? I can see that the implementation of invokelatest
indeed processes functions with kwargs differently. But the actual differences are in the core, which I’m unable to comprehend…
If there is a better way to make lazy loading of submodules, that would also solve the problem.
UPDATE
The problem turns out to be not in invokelatest
, but somewhere in how julia calls methods. Setting
function Base.getproperty(m::LazySubmodule, s::Symbol)
if s in (:_module_name, :_code, :_module)
return getfield(m, s)
end
Core.eval(m._module, :($(m._code)))
alias = m._code.args[2]
mod = Core.eval(m._module, :($(m._module_name) = $(alias)))
return (args...; kwargs...) -> error("This code is never reached")
end
shows that the result of the function is never used, and instead it’s substituted with T.M1.f1
directly.
UPDATE 2
The problem is in how Julia insides convert keyword functions to positional functions. While the lowered code for positional arguments looks fine:
@code_lowered T.f("A")
CodeInfo(
1 ─ %1 = Base.getproperty(Main.T.M1, :f1)
│ %2 = (%1)(s)
└── return %2
)
the code for functions with keywords is more complicated:
CodeInfo(
1 ─ %1 = (:s,)
│ %2 = Core.apply_type(Core.NamedTuple, %1)
│ %3 = Core.tuple(s)
│ %4 = (%2)(%3)
│ %5 = Base.getproperty(Main.T.M1, :f1)
│ %6 = Core.kwfunc(%5)
│ %7 = Base.getproperty(Main.T.M1, :f1)
│ %8 = (%6)(%4, %7)
└── return %8
)
So, essentially it calls Base.getproperty
twice. And as the module gets replaced between the two calls, only the first call uses invokelatest
. Still, I’d appreciate any advise on how to deal with that.