I don’t know if module-wise is impossible (not saying it isn’t), but could at calling site work?
@inline used to only work at the definition, and then later also at the caller site. I’m not sure how it works, or why, I think it’s still one (improved) macro… [I suppose it’s really two in one, and it needs to detect where it’s applied.]
@inline is not recursive (nor would you want it to be), but since it’s possible for the calling site, could you do the same, and even recursive…? I mean theoretically, not pushing you do it personally. And I mean @stable f(x) though also intriguing would be: stable using <module>, could such work?
Okay all is good, I fixed it here by moving back to the closure approach:
function _stable(fex::Expr)
func = splitdef(fex)
...
@gensym closure T
func[:body] = quote
let $closure() = $(func[:body]), $T = $(Base).promote_op($closure)
if !$(Base).isconcretetype($T)
...
end
return $closure()::$T
end
end
return combinedef(func)
end
It seems like promote_op gets the same results if you put all the function body in a closure. And, as before, the check looks to compile away if everything is type-stable.
I have not tried it, but maybe you can use @stable module A to rewire A.include(x) to make it default to A.include(f, x), where f is a map injecting @stable into function definitions?
Of course, also define the standard unwrap that returns just T for passing to f.
Probably
makestable(x) = x
makestable(::Type{T}) where {T} = StableType{T}()
unwrapstable(x) = x
unwrapstable(::StableType{T}) where {T} = T
then call stable_wrap(f, map(makestable, args...)...) and have then have stable_wrap itself call f(map(unwrapsbale, args...)...), along with the Base.promote_op(f, map(_typeof, args)...).
Is there any reason to avoid the closure approach above? It seems to work just as well, hugely simplifies the codebase, and also means one can safely use @stable on functions without variable names:
@stable f(x, ::Type{T}) where {T} = ...
whereas with a separate wrapper function, this would not be permissible.
@stable module A
using DispatchDoctor: @unstable
@unstable f1() = rand(Bool) ? 0 : 1.0
f2(x) = x
f3(; a=1) = a > 0 ? a : 0.0
include("other_code.jl")
end
@unstable turns it off (in case you have a function you need to be unstable)
With @thofma’s idea (thanks!), include(s) is automatically mapped to include(_stable_all_fnc, s) which propagates the @stable through the included code
Currently, neither function-level nor module-level @stable apply recursively to functions defined within the scope of other functions. In other words, g does not receive the @stable treatment in either of the following examples. Is this what you prefer?
@stable function f(xs)
g(x) = -2x
return map(g, xs)
end
@stable module A
function f(xs)
g(x) = -2x
return map(g, xs)
end
end
Perhaps it would be useful to allow using @stable on a begin-end block as well:
Since @stable only applies to functions, I think this may be the wrong pattern to use, as it might give the impression that it detects instability in arbitrary code, which it does not. To me module makes more sense because that’s where functions live.
(You should be able to use @stable explicitly on a closure function though)
It should, since all it does is put
function include(path::AbstractString)
return include(_stable_all_fnc, path)
end
at the top of the module definition. But note it doesn’t recursively apply to submodules.
There seem to be a couple of caveats regarding submodules. Firstly, every module and submodule within @stable requires its own using DispatchDoctor, otherwise you’ll get an error:
julia> @eval @stable module A
using DispatchDoctor
foo(x) = x
module B
bar(x) = -x
end
end
ERROR: syntax: module expression third argument must be a block
[...]
julia> @eval @stable module A
using DispatchDoctor
foo(x) = x
module B
using DispatchDoctor
bar(x) = -x
end
end
WARNING: replacing module A.
Main.A
Secondly, @stable will apply to functions defined in the same file as the module statement for the submodule (it just won’t propagate through include within the submodule). In the code above, A.B.bar gets the treatment:
Fair enough! I think applying to begin-end could be practically useful in many cases, say if you want only a subset of functions within a module to be @stable, perhaps every method of a function or every constructor for a type. But I get where you’re coming from. However, @unstable can be applied to a begin-end block as-is, so there’s a slight asymmetry there.
Oh yeah I spoke incorrectly. There’s actually no logic to skip submodules, so I guess it will work. It’s only that include within submodules won’t apply the transformations. The only time the recursion stops is for @stable (to avoid repeats) or @unstable. But other than that it will just apply to all function definitions it can find.
Should we add functionality to recursively overload include maybe? It should be pretty simple – can just add that overload on any module inside the wrapped expression.
Oh I think it’s not the import, but rather the fact that your module is a single line of code — the macro expects it to be a block. If you have any import there it should work. So it’s a bug but unrelated to the import (I think)