-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
_apply_in_world
builtin
#35844
_apply_in_world
builtin
#35844
Conversation
In discussion with Keno and Jeff, it became clear that the way precompile works (by collapsing world ages) means it's clear that nobody should be capturing the internal world age I suppose we might address this by introducing an opaque |
This'll be great to have for debuggers that want to simulate running generated functions. I'm not sure we want to add yet another allocation to the The point of precompile is that most likely the same world does not even exist after deserializing, and so it's taking the work that happened with a different view of the world and transforming it to be applicable to the new world. It's not just the value that can't be captured, but the whole state behind it that has no analogue (in the current implementation). |
Do you mean for the boxed I'm happy just to revert that part and live with a small amount of code duplication if people prefer. |
I added non-exported I also reverted to using the existing So from my perspective this is in a state where it could be merged. Also it would be great if people could try it out to see whether it helps their use cases in practice and we can iterate a bit more, if desired. |
@pfitzseb This is the PR I referred to which may help running a portion of text editor infrastructure in-process, without the user being able to change its functionality or cause latency spikes due to method invalidation. |
Cool! I'm probably not qualified to comment on the implementation, but it does look correct to me ;) |
New builtin Core._apply_in_world to allow frozen-world APIs to be implemented in pure Julia code. Also add an internal, experimental API Base.invoke_in_world() in analogy to Base.invokelatest() to make this easier to use (especially for keyword args) and to give us a place to put some documentation.
Ok I've rebased to fix some conflicts, and simplified as much as possible by removing the I plan to merge when tests pass so that people can try this in practice. |
Users shouldn't construct world ages themselves, only use the return type from get_current_world which is currently a UInt. Co-authored-by: Jameson Nash <[email protected]>
Thanks for taking another quick look Jameson 👍 I hope this can be of some use to people! |
Over at JuliaLang/Pkg.jl#1897, I was discussing how to use this with @timholy, but I thought I'd bring the answer here as it's relevant:
Yes, you can revise it in two different ways. Let me explain with the following code: # Suppose foo() is some library function
foo() = "First implementation of foo"
world1 = Base.get_world_counter()
# Suppose `bar()` is a public entry point to foo(), which wraps it to run in the user's session
# but provides a degree of separation.
# (For example, this could be the pkg REPL mode):
_entry_point_world = Base.get_world_counter()
function bar()
Base.invoke_in_world(_entry_point_world, foo)
end
@show foo()
@show bar()
# First way to work with the code interactively: call the implementation foo()
println("Now, revise foo:")
foo() = "Second implementation of foo"
@show foo()
@show bar()
# Second way to work with the code interactively: update the world age of the entry point
println("The public entry point world age can also be updated:")
_entry_point_world = Base.get_world_counter()
@show foo()
@show bar()
nothing Output from this code:
So you see you've got quite a lot of flexibility here. |
To state what's going on here in a different way: the world age is already dynamically scoped in nature. This means that you as the caller have the power to choose which world a computation runs in and |
I'm just curious how this interacts with tasks so I played with it a bit: foo() = "First implementation of foo"
function bar()
Base.invoke_in_world(_entry_point_world, foo)
end
afoo() = fetch(@async foo())
function abar()
Base.invoke_in_world(_entry_point_world, afoo)
end
# Note `@async` creates a closure so the counter has to be fetched after that.
_entry_point_world = Base.get_world_counter()
@show foo()
@show bar()
@show afoo()
@show abar()
println("Now, revise foo:")
foo() = "Second implementation of foo"
@show foo()
@show bar()
@show afoo()
@show abar() This prints
So, I guess the world age is very close to dynamic scoping although not quite until we have #35690? |
Excellent point. Yet more evidence to favor the solution proposed there. |
Okay. I just recorded this argument in #35690 (comment). |
Thanks for the explanation, @c42f. However, note my capitalization: I said I want to Revise it, not revise it. As it stands, if you edit
Cool feature, but if it's widely used this makes me nervous about maintaining Revise going forward. |
I don't think this is a problem: for normal use with Revise.jl, the key is that libraries do not use
|
New builtin `Core._apply_in_world` to allow frozen-world APIs to be implemented in pure Julia code. Also add an internal, experimental API `Base.invoke_in_world()` in analogy to `Base.invokelatest()` to make this easier to use (especially for keyword args) and to give us a place to put some documentation.
It seems like it would be nice to have something that would act like I think this is fairly readily implementable: perhaps we could create struct FrozenWorld
world::UInt
end and:
|
Most of the worlds encountered during precompile do not exist after deserialization, due to reordering and compression. We could have a mutable struct that updates the value, but what value would it have and what would usage look like? |
My thought is that we validate edges when we load the package, so any precompiled Example of usage: Revise could fix the world age in its internals to prevent invalidation by packages that the user loads. Thus acting kind of like |
This implements a new builtin
Core._apply_in_world
to allow Julia code to be run in a frozen world age (when combined withBase.get_world_counter()
).This is more general than
Core._apply_latest
, so we could remove_apply_latest
(or replace with a simple shim which calls_apply_in_world
withworld=typemax(UInt)
) if people think that's a good idea.Motivation
The original motivation for this was to have a way to freeze the world age of Julia code implementing the Julia parser - see note at #35243 (comment).
Using a fixed world should be beneficial for infrastructure code which runs in a user's julia process, but which is otherwise not expected to be modified by the user. There's two benefits:
$random_package
into their session.Note that world age is dynamically scoped, so fixed world would only apply when package code is entered through an
_apply_in_world
shim. Therefore devs of packages which choose to use this for deployment can still use a Revise-based workflow for developing their packages.Possible usage scenarios
pkg>
REPL mode, runpkg>
operations in subprocess? Pkg.jl#1816Questions / TODO
As public API, I've considered a callable
ApplyInWorld
function wrapper (and maybe an APIBase.freeze_world(f)
to create it) which would capture a function andBase.get_world_counter()
. There might be other options though? I considered making inference understand the builtin so that the ApplyInWorld shim could be inferred, but in discussions with @Keno and @vtjnash, it seemed there were difficulties with this. For one, Jameson thought capturing the world age in the type parameters ofApplyInWorld
would break subtyping in some way. For two, I was hoping this would help solve the problem that GeneralizedGenerated.jl solves, but Keno says that's not the case because inference can fundamentally only see older world ages. Actually I'm still a little confused by this because the newer world methods would be part of the global state so in principle accessible during compilation of an older world; but perhaps it just breaks fundamental inference invariants I don't understand yet.A few todos:
ReplaceReverted to avoid an extra allocation_apply_latest
builtin with thisApplyInWorld
wrappers from being saved during precompilation? (Edit: yes)UInt
or does this create an implicit dependency on old methods which may be removed from internal caches? Do we need to lift this implicit dependency into an explicit one? (Edit: Should be ok?)@vtjnash I'd greatly value your input on subtleties I might have missed, especially on the last two questions above.