-
-
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
WIP: World-age partition bindings #54654
base: master
Are you sure you want to change the base?
Conversation
Summarizing discussion with @JeffBezanson (only part of it) @StefanKarpinski @vtjnash @topolarity @gbaraldi @oscardssmith from today about the remaining semantic questions:
When does binding resolution happen semantically?Example 1Right now, the precise point-in-time of binding resolution is ill-defined. To illustrate this, consider:
but also
Basically, right now bindings get resolved whenever anything in the system happens to look at a binding, but since the running-or-not of inference is outside the semantics of the language, these semantics are ill-defined. Example 2Similar for binding ambiguousness:
with
(similar to example 1, the explicit reference of Proposed semanticsThe proposed semantics are (and this was not fully spelled out in the discussion, so there may be some further debate on this) that bindings resolve (in decreasing order of priority)
Note 1: Only toplevel To see concrete effects, the assignment in Example 1, Overall, these semantics remove any consideration of code-execution order (with the exception of the ordering of world-age-incrementing top-level declarations) and should be a lot clearer. There should also be no change in binding resolution in cases that are not currently errors or warnings. Cases that are may change slightly, but as discussed above, we currently don't actually guarantee those resolutions, because binding resolution may happen at any time. The one wrinkle here is that currently, there is precisely one case where bindings are introduced by non-toplevel code as discovered in #54607. We discussed this somewhat extensively. This change was introduced in 1.9 to allow modification of existing globals in other modules, but accidentally also permitted the creation of new bindings. The overall consensus was the independent of any semantics changes here, we need to correct this oversight, I will be submitting a PR shortly to attempt to correct this issue for 1.11, though we may have to go through a deprecation since it was in the system for two releases. Regardless, it shouldn't be an impediment for this change. What to do about replacement of mutable bindings?The semantics of binding replacement for const bindings are fairly clear and match the semantics of method replacement reasonably closely: The values that you see are the values that happened to be assigned when the world age is captured. However, this issue becomes trickier with mutable bindings. Here are some examples (I'll be using opaque closures as the canonical world age capture mechanism, but feel free to substitute tasks or whatever other world-age capture mechanism you prefer). ExampleFor example, what does the following do:
Now, consider the following:
As implemented in this PR, bindings are fully partitioned, so this would give Other optionsFor completeness, I will list all the options we discussed, although some of these are probably bad: Suggestion 1: Merge bindings with egal metadata across world ages.This is not quite relevant to the example, but there was a proposal that in:
the bindings in world ages 1 and 3 should alias. I think we largely discarded this proposal as
Suggestion 2: setglobal! assigns in every compatible world ageBasically, the semantics here would be
you would get We liked this semantically, but were concerned about the difficulty of implementation. Suggestion 3: Writing outdated mutable bindings is an errorRelatively straightforward. If you replace a binding, then from that point on, all code running in old world ages will error upon assignment. In our running example, we would have
@topolarity raised the point that it seems odd to disallow this at top level, while permitting mutations through mutable objects, but that same concern applies to mutable values accessed through Suggestion 4: Do that, but also error on readsBasically, as soon as you replace a mutable binding, the old one becomes toxic and loudly errors. In our running example, this gives
This situation is not super common, particularly since global mutable state is generally discouraged in Julia (as in other languages). An open question is whether we want to extend this behavior to Suggestion 5 [late submission]: type assert (using the old binding type) on read and writeSimilar to suggestion 4, but rather than erroring unconditionally, old world bindings would gain typeasserts (on read with the old type, on write with the new type). ConclusionI think we arrived at starting with suggestion 4. It's the most conservative and we should explicitly reserve the potential of revisiting the error cases in the future. Suggestion 2 was also well liked, but implementation difficulty was a concern. Additionally, with the indicated semantic reservation, switching from suggestion 4 to suggestion 2 is feasible, but not vice versa. Eager resolution of bindings?@JeffBezanson is concerned about losing the redefinition error in the following case:
also known as the
Should we do guard entries?I originally made this a semantic question, although it's really more of a performance consideration. That said, with the changes to binding resolution discussed above, I believe guard entries are required for correctness anyway, so I think this question is moot. |
As discussed in [1], the implicit creation of bindings through the setglobal! intrinsic was accidentally added in 1.9 unintentionally and will be removed (ideally) or at the very least deprecated in 1.11. The recommended replacement syntax is `Core.eval(mod, Expr(:global, sym))` to introduce the binding and `invokelatest(setglobal!, mod, sym, val)` to set it. The invokelatest is not presently required, but may be required for JuliaLang/julia#54654, so it's included in the recommendation. [1] JuliaLang/julia#54607
PR #44231 (part of Julia 1.9) introduced the ability to modify globals with `Mod.sym = val` syntax. However, the intention of this syntax was always to modify *existing* globals in other modules. Unfortunately, as implemented, it also implicitly creates new bindings in the other module, even if the binding was not previously declared. This was not intended, but it's a bit of a syntax corner case, so nobody caught it at the time. After some extensive discussions and taking into account the near future direction we want to go with bindings (#54654 for both), the consensus was reached that we should try to undo the implicit creation of bindings (but not the ability to assign the *value* of globals in other modules). Note that this was always an error until Julia 1.9, so hopefully it hasn't crept into too many packages yet. We'll see what pkgeval says. If use is extensive, we may want to consider a softer removal strategy. Across base and stdlib, there's two cases affected by this change: 1. A left over debug statement in `precompile` that wanted to assign a new variable in Base for debugging. Removed in this PR. 2. Distributed wanting to create new bindings. This is a legimitate use case for wanting to create bindings in other modules. This is fixed in JuliaLang/Distributed.jl#102. As noted in that PR, the recommended replacement where implicit binding creation is desired is: ``` Core.eval(mod, Expr(:global, sym)) invokelatest(setglobal!, mod, sym, val) ``` The `invokelatest` is not presently required, but may be needed by #54654, so it's included in the recommendation now. Fixes #54607
I am not sure about any resolution that will cause code running to error.
I am not convinced that this situation isn't common and it would prohibit any use of global mutable bindings by any code that executes in a separate/frozen world-ages. We might in due time want to be able to execute "compilation unit" that are fully separated from the rest of the system.
I do understand the concerns, but for me the conclusion seems more to be that globals must be accessed in the world-age of the task / or long running tasks must be restarted. I am not convinced that the Revise use-case outweighs the statement: "Old code must be able to keep running" |
Pretty much yes. I will admit that I generally think mutable globals are the wrong tool for anything beyond basic repl usage and I think of frozen world ages as a somewhat advanced feature, so I don't mind putting the complexity there as much. That said, separate compilation units can of course have their own entirely separate semantics and not observe global assignments from outside at all.
What about |
As discussed in [1], the implicit creation of bindings through the setglobal! intrinsic was accidentally added in 1.9 unintentionally and will be removed (ideally) or at the very least deprecated in 1.11. The recommended replacement syntax is `Core.eval(mod, Expr(:global, sym))` to introduce the binding and `invokelatest(setglobal!, mod, sym, val)` to set it. The invokelatest is not presently required, but may be required for JuliaLang/julia#54654, so it's included in the recommendation. [1] JuliaLang/julia#54607
It feels weird to add extra runtime overhead to a language feature. In some sense I would want to keep the current semantics that long-running tasks must opt-in to seeing "new"/"unexpected" state with
That is the scenario we are worried about? If I now change For me that is equivalent to
Of course you raised this point in your proposal:
For me information shouldn't travel back in time and thus anything we do here can't impact code executing in a prior world-age. I would rather make the rule simple: "If you modify a binding, that modification is only visible from here on out" and not have complicated rules about unifying bindings across time. The "right" way fro me to write long-running tasks for that in a hot reloading scenario is probably:
|
Yes, or even something more benign like
Yes, that was also what I originally wanted to do and is what is implemented in this PR, but you and I both have a very sophisticated understanding of world ages. I fear that most users may not. |
One of the biggest problems I have is that "modifying a binding" is not necessarily an explicit operation. From the users perspective, they just changed the type on a binding, or even worse in the:
case, the user may not be touching the binding at all at the source level, but revise still has to do the rebinding. |
Absolutely! For me the question is if the Revise use-case trumps :
For me |
I don't think it's really about Revise. Revise is fine with either semantics. The question is more about the semantics of old-world code, for which there are two competing priorities:
For 1, I really don't think that the typeassert solution is that bad. In the pre-typed-globals world (which is still extremely common), people would regularly do things like |
Said another way, I think the issue is mostly about type restriction on globals. If we didn't have that feature, I think the answer would be fairly obvious that there's only one mutable location for a binding (even if you can shadow it with a const for some subset of the world age). So then the question is if the type restriction feature is compelling enough of a reason to world-age fracture by type restriction semantically, and it just seems too niche for that to be realistic. I think the semantics of:
seems like a very simple mental model. |
I think I like that last approach, but I want to be sure what it means. Is the idea:
Is that what you have in mind here, @Keno? One thing to consider is: if the user writes |
Thinking about it some more this morning, I think "Suggestion 5" may be workable. It allows changing |
Yes
I wasn't suggesting the additional convert on access (only assignment), since we don't usually convert on access. Although arguably we don't have to, since the can never have a mismatch.
Yes
Yes
Yes, modulo above question on convert-on-access
Yes |
Thinking about this some more (and consistent with what I wrote yesterday), I don't think we can introduce old-world converts to/from the new type either on access or on write, because in general the new type may be defined in a new world, so the convert is likely to fail. Of course, we could implicitly transition to the latest world, but I think that's too magical. I think the only semantics that are sensible are:
Where |
PR #44231 (part of Julia 1.9) introduced the ability to modify globals with `Mod.sym = val` syntax. However, the intention of this syntax was always to modify *existing* globals in other modules. Unfortunately, as implemented, it also implicitly creates new bindings in the other module, even if the binding was not previously declared. This was not intended, but it's a bit of a syntax corner case, so nobody caught it at the time. After some extensive discussions and taking into account the near future direction we want to go with bindings (#54654 for both), the consensus was reached that we should try to undo the implicit creation of bindings (but not the ability to assign the *value* of globals in other modules). Note that this was always an error until Julia 1.9, so hopefully it hasn't crept into too many packages yet. We'll see what pkgeval says. If use is extensive, we may want to consider a softer removal strategy. Across base and stdlib, there's two cases affected by this change: 1. A left over debug statement in `precompile` that wanted to assign a new variable in Base for debugging. Removed in this PR. 2. Distributed wanting to create new bindings. This is a legimitate use case for wanting to create bindings in other modules. This is fixed in JuliaLang/Distributed.jl#102. As noted in that PR, the recommended replacement where implicit binding creation is desired is: ``` Core.eval(mod, Expr(:global, sym)) invokelatest(setglobal!, mod, sym, val) ``` The `invokelatest` is not presently required, but may be needed by #54654, so it's included in the recommendation now. Fixes #54607
The reason I mentioned convert-on-access is because you could have a situation like this: global x::Any # world 1
global x::String # world 2
global x::Float64 # world 3
global x::Int # world 4 What happens when one does |
I think doing anything beyond the standard conversion on assignment we do right now is too magical. |
So basically:
A couple of clarifying questions:
|
Basically, the way to think about it is that there is only one storage location for
yes
There's only one storage location - if the actual stored value is not compatible with the binding type that was declared in a previous world age, access becomes an error.
Convert according to the old binding type and then attempt to assign (without convert) into the global. If the type (after convert in the old world) is not compatible with the latest declared type (without any convert), this is an error.
There's no scanning of historical worlds, the typeassert is on access. |
PR #44231 (part of Julia 1.9) introduced the ability to modify globals with `Mod.sym = val` syntax. However, the intention of this syntax was always to modify *existing* globals in other modules. Unfortunately, as implemented, it also implicitly creates new bindings in the other module, even if the binding was not previously declared. This was not intended, but it's a bit of a syntax corner case, so nobody caught it at the time. After some extensive discussions and taking into account the near future direction we want to go with bindings (#54654 for both), the consensus was reached that we should try to undo the implicit creation of bindings (but not the ability to assign the *value* of globals in other modules). Note that this was always an error until Julia 1.9, so hopefully it hasn't crept into too many packages yet. We'll see what pkgeval says. If use is extensive, we may want to consider a softer removal strategy. Across base and stdlib, there's two cases affected by this change: 1. A left over debug statement in `precompile` that wanted to assign a new variable in Base for debugging. Removed in this PR. 2. Distributed wanting to create new bindings. This is a legimitate use case for wanting to create bindings in other modules. This is fixed in JuliaLang/Distributed.jl#102. As noted in that PR, the recommended replacement where implicit binding creation is desired is: ``` Core.eval(mod, Expr(:global, sym)) invokelatest(setglobal!, mod, sym, val) ``` The `invokelatest` is not presently required, but may be needed by #54654, so it's included in the recommendation now. Fixes #54607
Does this have to be an error? I would have expected this to be an error upon access in the new world, rather than modification in the old one. Otherwise, it seems like |
It is feasible to make it symmetric, but then every new world access will need to have a type-assert on read, even if the binding hasn't been replaced, which has performance implications. I also think I like the semantics of erroring in the old world better, but I'm open to discussion.
It tries to convert, and is an error if not possible. |
I guess we could cache the typeassert for the newest world along with the world age and collapse the check into one. So yeah, I think either is probably feasible. Your proposal does have the advantage of letting old code keep running if the new code never actually touches the global. |
This is a prepratory commit for #54654 to change the lowering of `const` and typed globals to be compatible with the new semantics. Currently, we lower `const a::T = val` to: ``` const a global a::T a = val ``` (which further expands to typed-globals an implicit converts). This works, because, under the hood, our const declarations are actually assign-once globals. Note however, that this is not syntactically reachable, since we have a parse error for plain `const a`: ``` julia> const a ERROR: ParseError: # Error @ REPL[1]:1:1 const a └─────┘ ── expected assignment after `const` Stacktrace: [1] top-level scope @ none:1 ``` However, this lowering is not atomic with respect to world age. The semantics in #54654 require that the const-ness and the value are established atomically (with respect to world age, potentially on another thread) or undergo invalidation. To resolve this issue, this PR changes the lowering of `const a::T = val` to: ``` let local a::T = val const (global a) = a end ``` where the latter is a special syntax form `Expr(:const, GlobalRef(,:a), :a)`. A similar change is made to const global declarations, which previously lowered via intrinsic, i.e. `global a::T = val` lowered to: ``` global a Core.set_binding_type!(Main, :a, T) _T = Core.get_binding_type(Main, :a) if !isa(val, _T) val = convert(_T, val) end a = val ``` This changes the `set_binding_type!` to instead be a syntax form `Expr(:globaldecl, :a, T)`. This is not technically required, but we currently do not use intrinsics for world-age affecting side-effects anywhere else in the system. In particular, after #54654, it would be illegal to call `set_binding_type!` in anything but top-level context. Now, we have discussed in the past that there should potentially be intrinsic functions for global modifications (method table additions, etc), currently only reachable through `Core.eval`, but such an intrinsic would require semantics that differ from both the current `set_binding_type!` and the new `:globaldecl`. Using an Expr form here is the most consistent with our current practice for these sort of things elsewhere and accordingly, this PR removes the intrinsic. Note that this PR does not yet change any syntax semantics, although there could in principle be a reordering of side-effects within an expression (e.g. things like `global a::(@isdefined(a) ? Int : Float64)` might behave differently after this commit. However, we never defined the order of side effects (which is part of what this is cleaning up, although, I am not formally defining any specific ordering here either - #54654 will do some of that), and that is not a common case, so this PR should be largely considered non-semantic with respect to the syntax change.
This is a prepratory commit for #54654 to change the lowering of `const` and typed globals to be compatible with the new semantics. Currently, we lower `const a::T = val` to: ``` const a global a::T a = val ``` (which further expands to typed-globals an implicit converts). This works, because, under the hood, our const declarations are actually assign-once globals. Note however, that this is not syntactically reachable, since we have a parse error for plain `const a`: ``` julia> const a ERROR: ParseError: # Error @ REPL[1]:1:1 const a └─────┘ ── expected assignment after `const` Stacktrace: [1] top-level scope @ none:1 ``` However, this lowering is not atomic with respect to world age. The semantics in #54654 require that the const-ness and the value are established atomically (with respect to world age, potentially on another thread) or undergo invalidation. To resolve this issue, this PR changes the lowering of `const a::T = val` to: ``` let local a::T = val const (global a) = a end ``` where the latter is a special syntax form `Expr(:const, GlobalRef(,:a), :a)`. A similar change is made to const global declarations, which previously lowered via intrinsic, i.e. `global a::T = val` lowered to: ``` global a Core.set_binding_type!(Main, :a, T) _T = Core.get_binding_type(Main, :a) if !isa(val, _T) val = convert(_T, val) end a = val ``` This changes the `set_binding_type!` to instead be a syntax form `Expr(:globaldecl, :a, T)`. This is not technically required, but we currently do not use intrinsics for world-age affecting side-effects anywhere else in the system. In particular, after #54654, it would be illegal to call `set_binding_type!` in anything but top-level context. Now, we have discussed in the past that there should potentially be intrinsic functions for global modifications (method table additions, etc), currently only reachable through `Core.eval`, but such an intrinsic would require semantics that differ from both the current `set_binding_type!` and the new `:globaldecl`. Using an Expr form here is the most consistent with our current practice for these sort of things elsewhere and accordingly, this PR removes the intrinsic. Note that this PR does not yet change any syntax semantics, although there could in principle be a reordering of side-effects within an expression (e.g. things like `global a::(@isdefined(a) ? Int : Float64)` might behave differently after this commit. However, we never defined the order of side effects (which is part of what this is cleaning up, although, I am not formally defining any specific ordering here either - #54654 will do some of that), and that is not a common case, so this PR should be largely considered non-semantic with respect to the syntax change.
This is a prepratory commit for #54654 to change the lowering of `const` and typed globals to be compatible with the new semantics. Currently, we lower `const a::T = val` to: ``` const a global a::T a = val ``` (which further expands to typed-globals an implicit converts). This works, because, under the hood, our const declarations are actually assign-once globals. Note however, that this is not syntactically reachable, since we have a parse error for plain `const a`: ``` julia> const a ERROR: ParseError: # Error @ REPL[1]:1:1 const a └─────┘ ── expected assignment after `const` Stacktrace: [1] top-level scope @ none:1 ``` However, this lowering is not atomic with respect to world age. The semantics in #54654 require that the const-ness and the value are established atomically (with respect to world age, potentially on another thread) or undergo invalidation. To resolve this issue, this PR changes the lowering of `const a::T = val` to: ``` let local a::T = val const (global a) = a end ``` where the latter is a special syntax form `Expr(:const, GlobalRef(,:a), :a)`. A similar change is made to const global declarations, which previously lowered via intrinsic, i.e. `global a::T = val` lowered to: ``` global a Core.set_binding_type!(Main, :a, T) _T = Core.get_binding_type(Main, :a) if !isa(val, _T) val = convert(_T, val) end a = val ``` This changes the `set_binding_type!` to instead be a syntax form `Expr(:globaldecl, :a, T)`. This is not technically required, but we currently do not use intrinsics for world-age affecting side-effects anywhere else in the system. In particular, after #54654, it would be illegal to call `set_binding_type!` in anything but top-level context. Now, we have discussed in the past that there should potentially be intrinsic functions for global modifications (method table additions, etc), currently only reachable through `Core.eval`, but such an intrinsic would require semantics that differ from both the current `set_binding_type!` and the new `:globaldecl`. Using an Expr form here is the most consistent with our current practice for these sort of things elsewhere and accordingly, this PR removes the intrinsic. Note that this PR does not yet change any syntax semantics, although there could in principle be a reordering of side-effects within an expression (e.g. things like `global a::(@isdefined(a) ? Int : Float64)` might behave differently after this commit. However, we never defined the order of side effects (which is part of what this is cleaning up, although, I am not formally defining any specific ordering here either - #54654 will do some of that), and that is not a common case, so this PR should be largely considered non-semantic with respect to the syntax change. Also fixes #54787 while we're at it.
This is a re-worked extraction of #54654, adjusted to support the new semantics arrived at over the course of that thread. Note that this is the data-structure change only. The semantics in this PR still match current master to the greatest extent possible. In particular, this does not fully implement thread-safety of the old semantics, since that works differently after #54654 (this PR essentially implements the old semantics by mutating in place data structures that are supposed to be immutable), though note that there were some thread safety concerns in the existing also. The core idea here is to split `Binding` in two: A new `Binding` with minimal data and a `BindingPartition` that holds all data that is world-age partitioned. In the present PR, these are always in 1:1 correspondednce, but after #54654, there will be multiple `BindingPartition`s for every `Binding`. Essentially the `owner` and `ty` fields have been merged into a new `restriction` field of `BindingPartition`, which may also hold the value of a constant (consistent with the final semantics reached in #54654). The non-partitioned binding->value field is now used exclusively for non-constant globals. The disambiguation for how to interpret the `restriction` field happens via flags. `->imported` grew to 2 bits and can now be one of `NONE`/`IMPLICIT`/ `EXPLICIT`/`GUARD`. `GUARD` corresponds to the old `b->owner == NULL` case. `NONE` corresponds to the old `b->owner == b` case, while IMPLICIT/EXPLICIT correspond to `b->owner != b` and the old `imported` flag. Other than that, the behavior of the flags is unchanged. Additionally, fields are provided for `min_world`/`max_world`/`next` and a `declared` flag in `Binding`, but these are unused in this PR and prepratory only. Because of above mentioned thread-safety concerns, I am not anticipating merging this PR by itself, but I think it is a good review partition to cut down on the number of changes in the main PRs.
Currently we error when attempting to serialize Bindings that do not beloing to the incremental module (GlobalRefs have special logic to avoid looking at the binding field). With #54654, Bindings will show up in more places, so let's just unique them properly by their module/name identity. Of course, we then have two objects so serialized (both GlobalRef and Binding), which suggests that we should perhaps finish the project of unifying them. This is not currently possible, because the existence of a binding object in the binding table has semantic content, but this will change with #54654, so we can do such a change thereafter.
This is a follow up to resolve a TODO left in #54773 as part of preparatory work for #54654. Currently, our lowering for type definition contains an early `isdefined` that forces a decision on binding resolution before the assignment of the actual binding. In the current implementation, this doesn't matter much, but with #54654, this would incur a binding invalidation we would like to avoid. To get around this, we extend the (internal) `isdefined` form to take an extra argument specifying whether or not to permit looking at imported bindings. If not, resolving the binding is not required semantically, but for the purposes of type definition (where assigning to an imported binding would error anyway), this is all we need.
This is a follow up to resolve a TODO left in #54773 as part of preparatory work for #54654. Currently, our lowering for type definition contains an early `isdefined` that forces a decision on binding resolution before the assignment of the actual binding. In the current implementation, this doesn't matter much, but with #54654, this would incur a binding invalidation we would like to avoid. To get around this, we extend the (internal) `isdefined` form to take an extra argument specifying whether or not to permit looking at imported bindings. If not, resolving the binding is not required semantically, but for the purposes of type definition (where assigning to an imported binding would error anyway), this is all we need.
This is a follow up to resolve a TODO left in #54773 as part of preparatory work for #54654. Currently, our lowering for type definition contains an early `isdefined` that forces a decision on binding resolution before the assignment of the actual binding. In the current implementation, this doesn't matter much, but with #54654, this would incur a binding invalidation we would like to avoid. To get around this, we extend the (internal) `isdefined` form to take an extra argument specifying whether or not to permit looking at imported bindings. If not, resolving the binding is not required semantically, but for the purposes of type definition (where assigning to an imported binding would error anyway), this is all we need.
This is a follow up to resolve a TODO left in #54773 as part of preparatory work for #54654. Currently, our lowering for type definition contains an early `isdefined` that forces a decision on binding resolution before the assignment of the actual binding. In the current implementation, this doesn't matter much, but with #54654, this would incur a binding invalidation we would like to avoid. To get around this, we extend the (internal) `isdefined` form to take an extra argument specifying whether or not to permit looking at imported bindings. If not, resolving the binding is not required semantically, but for the purposes of type definition (where assigning to an imported binding would error anyway), this is all we need.
Currently we error when attempting to serialize Bindings that do not beloing to the incremental module (GlobalRefs have special logic to avoid looking at the binding field). With #54654, Bindings will show up in more places, so let's just unique them properly by their module/name identity. Of course, we then have two objects so serialized (both GlobalRef and Binding), which suggests that we should perhaps finish the project of unifying them. This is not currently possible, because the existence of a binding object in the binding table has semantic content, but this will change with #54654, so we can do such a change thereafter.
This is a follow up to resolve a TODO left in #54773 as part of preparatory work for #54654. Currently, our lowering for type definition contains an early `isdefined` that forces a decision on binding resolution before the assignment of the actual binding. In the current implementation, this doesn't matter much, but with #54654, this would incur a binding invalidation we would like to avoid. To get around this, we extend the (internal) `isdefined` form to take an extra argument specifying whether or not to permit looking at imported bindings. If not, resolving the binding is not required semantically, but for the purposes of type definition (where assigning to an imported binding would error anyway), this is all we need.
This is a follow up to resolve a TODO left in #54773 as part of preparatory work for #54654. Currently, our lowering for type definition contains an early `isdefined` that forces a decision on binding resolution before the assignment of the actual binding. In the current implementation, this doesn't matter much, but with #54654, this would incur a binding invalidation we would like to avoid. To get around this, we extend the (internal) `isdefined` form to take an extra argument specifying whether or not to permit looking at imported bindings. If not, resolving the binding is not required semantically, but for the purposes of type definition (where assigning to an imported binding would error anyway), this is all we need.
This is a follow up to resolve a TODO left in #54773 as part of preparatory work for #54654. Currently, our lowering for type definition contains an early `isdefined` that forces a decision on binding resolution before the assignment of the actual binding. In the current implementation, this doesn't matter much, but with #54654, this would incur a binding invalidation we would like to avoid. To get around this, we extend the (internal) `isdefined` form to take an extra argument specifying whether or not to permit looking at imported bindings. If not, resolving the binding is not required semantically, but for the purposes of type definition (where assigning to an imported binding would error anyway), this is all we need.
Upon discussion at JuliaCon, this seems more workable than I feared. My one remaining concern is whether it will still be possible to re-evaluate the same (unchanged) structure definition without redefining the binding and invalidating the old type. Revise currently does a lot of that en route to its quest to compute accurate method signatures (which often involve creating new structs). Currently if you re-evaluate the same method definition it does invalidate the old one and create a new (equivalent) one. Revise works hard to avoid doing this, but doesn't take the same care for types because types are needed for signatures and re-evaluating them currently doesn't trigger any invalidation. (Just an error message if they've changed.) |
I didn't find it during the discussion, but I found another implicit way to resolve the binding using tab-completions: julia> module A
function foo end
export foo
end
Main.A
julia> module B
function foo end
export foo
end
Main.B
julia> using .A
julia> foo(<TAB> # after pressing `TAB` simply remove the line but do not call foo
julia> using .B
WARNING: using B.foo in module Main conflicts with an existing identifier. The warning suggests that the binding for julia> module A
function foo end
export foo
end
Main.A
julia> module B
function foo end
export foo
end
Main.B
julia> using .A
julia> using .B I don't know if its an issue here, just an interesting observation that may be relevant to the discussion. |
Yes, it was discussed. This will change to no longer implicitly resolve the binding. |
This is a re-worked extraction of #54654, adjusted to support the new semantics arrived at over the course of that thread. Note that this is the data-structure change only. The semantics in this PR still match current master to the greatest extent possible. The core idea here is to split `Binding` in two: A new `Binding` with minimal data and a `BindingPartition` that holds all data that is world-age partitioned. In the present PR, these are always in 1:1 correspondednce, but after #54654, there will be multiple `BindingPartition`s for every `Binding`. Essentially the `owner` and `ty` fields have been merged into a new `restriction` field of `BindingPartition`, which may also hold the value of a constant (consistent with the final semantics reached in #54654). The non-partitioned binding->value field is now used exclusively for non-constant globals. The disambiguation for how to interpret the `restriction` field happens via flags. `->imported` grew to 2 bits and can now be one of `NONE`/`IMPLICIT`/ `EXPLICIT`/`GUARD`. `GUARD` corresponds to the old `b->owner == NULL` case. `NONE` corresponds to the old `b->owner == b` case, while IMPLICIT/EXPLICIT correspond to `b->owner != b` and the old `imported` flag. Other than that, the behavior of the flags is unchanged. Additionally, fields are provided for `min_world`/`max_world`/`next`, but these are unused in this PR and prepratory only.
This is a re-worked extraction of #54654, adjusted to support the new semantics arrived at over the course of that thread. Note that this is the data-structure change only. The semantics in this PR still match current master to the greatest extent possible. The core idea here is to split `Binding` in two: A new `Binding` with minimal data and a `BindingPartition` that holds all data that is world-age partitioned. In the present PR, these are always in 1:1 correspondednce, but after #54654, there will be multiple `BindingPartition`s for every `Binding`. Essentially the `owner` and `ty` fields have been merged into a new `restriction` field of `BindingPartition`, which may also hold the value of a constant (consistent with the final semantics reached in #54654). The non-partitioned binding->value field is now used exclusively for non-constant globals. The disambiguation for how to interpret the `restriction` field happens via flags. `->imported` grew to 2 bits and can now be one of `NONE`/`IMPLICIT`/ `EXPLICIT`/`GUARD`. `GUARD` corresponds to the old `b->owner == NULL` case. `NONE` corresponds to the old `b->owner == b` case, while IMPLICIT/EXPLICIT correspond to `b->owner != b` and the old `imported` flag. Other than that, the behavior of the flags is unchanged. Additionally, fields are provided for `min_world`/`max_world`/`next`, but these are unused in this PR and prepratory only.
Now that I've had a few months to recover from the slog of adding `BindingPartition`, it's time to renew my quest to finish #54654. This adds the basic infrastructure for having multiple partitions, including making the lookup respect the `world` argument - on-demand allocation of missing partitions, `Base.delete_binding` and the `@world` macro. Not included is any inference or invalidation support, or any support for the runtime to create partitions itself (only `Base.delete_binding` does that for now), which will come in subsequent PRs.
Now that I've had a few months to recover from the slog of adding `BindingPartition`, it's time to renew my quest to finish #54654. This adds the basic infrastructure for having multiple partitions, including making the lookup respect the `world` argument - on-demand allocation of missing partitions, `Base.delete_binding` and the `@world` macro. Not included is any inference or invalidation support, or any support for the runtime to create partitions itself (only `Base.delete_binding` does that for now), which will come in subsequent PRs.
Now that I've had a few months to recover from the slog of adding `BindingPartition`, it's time to renew my quest to finish #54654. This adds the basic infrastructure for having multiple partitions, including making the lookup respect the `world` argument - on-demand allocation of missing partitions, `Base.delete_binding` and the `@world` macro. Not included is any inference or invalidation support, or any support for the runtime to create partitions itself (only `Base.delete_binding` does that for now), which will come in subsequent PRs.
Now that I've had a few months to recover from the slog of adding `BindingPartition`, it's time to renew my quest to finish #54654. This adds the basic infrastructure for having multiple partitions, including making the lookup respect the `world` argument - on-demand allocation of missing partitions, `Base.delete_binding` and the `@world` macro. Not included is any inference or invalidation support, or any support for the runtime to create partitions itself (only `Base.delete_binding` does that for now), which will come in subsequent PRs.
Now that I've had a few months to recover from the slog of adding `BindingPartition`, it's time to renew my quest to finish #54654. This adds the basic infrastructure for having multiple partitions, including making the lookup respect the `world` argument - on-demand allocation of missing partitions, `Base.delete_binding` and the `@world` macro. Not included is any inference or invalidation support, or any support for the runtime to create partitions itself (only `Base.delete_binding` does that for now), which will come in subsequent PRs.
Now that I've had a few months to recover from the slog of adding `BindingPartition`, it's time to renew my quest to finish #54654. This adds the basic infrastructure for having multiple partitions, including making the lookup respect the `world` argument - on-demand allocation of missing partitions, `Base.delete_binding` and the `@world` macro. Not included is any inference or invalidation support, or any support for the runtime to create partitions itself (only `Base.delete_binding` does that for now), which will come in subsequent PRs.
This implements world-age partitioning of bindings as proposed in #40399. In effect, much like methods, the global view of bindings now depends on your currently executing world. This means that
const
bindings can now have different values in different worlds. In principle it also means that regular global variables could have different values in different worlds, but there is currently no case where the system does this.Motivation
The reasons for this change are manifold:
The primary motivation is to permit Revise to redefine structs. This has been a feature request since the very begining of Revise (redefining struct timholy/Revise.jl#18) and there have been numerous attempts over the past 7 years to address this, as well as countless duplicate feature request. A past attempt to implement the necessary julia support in Support type renaming #22721 failed because the consequences and semantics of re-defining bindings were not sufficiently worked out. One way to think of this implementation (at least with respect to types) is that it provides a well-grounded implementation of Support type renaming #22721.
A secondary motivation is to make
const
-redefinition no longer UB (althoughconst
redefinition will still have a significant performance penalty, so it is not recommended). See e.g. the full discussion in Add devdocs on UB #54099 and Behavior of reassignment to aconst
#38584.Not currently implemented, but this mechanism can be used to re-compile code where bindings are introduced after the first compile, which is a common performance trap for new users (Track backedges through not-yet-defined bindings? #53958).
Not currently implemented, but this mechanism can be used to clarify the semantics of bindings import and resolution to address issues like compiling top-level expressions forces global binding resolution #14055.
Implementation
In this PR:
Binding
getsmin_world
/max_world
fields likeCodeInstance
const
globals in value position, but if binding replacement is permitted, the validity of this may change after the fact. To address this, there is a helper inCore.Compiler
that gets invoked in the type inference world and will rewrite the method source to be legal in all worlds.@world
macro can be used to access bindings from old world ages. This is used in printing for old objects.const
-override behavior was changed to only be permitted at toplevel. The warnings about it being UB was removed.Of particular note, this PR does not include any mechanism for invalidating methods whose signatures were created using an old Binding (or types whose fields were the result of a binding evaluation). There was some discussion among the compiler team of whether such a mechanism should exist in base, but the consensus was that it should not. In particular, although uncommon, a pattern like:
Does not redefine
g
. Thus to fully address the Revise issue, additional code will be required in Revise to track the dependency of various signatures and struct definitions on bindings.Demo
Performance consideration
On my machine, the validation required upon binding replacement for the full system image takes about 200ms. With CedarSim loaded (I tried OmniPackage, but it's not working on master), this increases about 5x. That's a fair bit of compute, but not the end of the world. Still, Revise may have to batch its validation. There may also be opportunities for performance improvement by operating on the compressed representation directly.
Semantic TODO
Implementation TODO
using
by world age as wellconst
/global
lowering changes [lowering: Refactor lowering for const and typed globals #54773]BindingPartition
type [RefactorBinding
data structures in preparation for partition #54788]global a