-
-
Notifications
You must be signed in to change notification settings - Fork 5.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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. The reasons for this change are manifold: 1. The primary motivation is to permit Revise to redefine structs. This has been a feature request since the very begining of Revise (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 #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 #22721. 2. A secondary motivation is to make `const`-redefinition no longer UB (although `const` redefinition will still have a significant performance penalty, so it is not recommended). See e.g. the full discussion in #54099. 3. 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 (#53958). 4. Not currently implemented, but this mechanism can be used to clarify the semantics of bindings import and resolution to address issues like #14055. In this PR: - `Binding` gets `min_world`/`max_world` fields like `CodeInstance` - Various lookup functions walk this linked list using the current task world_age as a key - Inference accumulates world bounds as it would for methods - Upon binding replacement, we walk all methods in the system, invalidating those whose uninferred IR references the replaced GlobalRef - One primary complication is that our IR definition permits `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 in `Core.Compiler` that gets invoked in the type inference world and will rewrite the method source to be legal in all worlds. - A new `@world` macro can be used to access bindings from old world ages. This is used in printing for old objects. - The `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: ``` f() = Any g(::f()) = 1 f() = Int ``` 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. ``` julia> struct Foo a::Int end julia> g() = Foo(1) g (generic function with 1 method) julia> g() Foo(1) julia> f(::Foo) = 1 f (generic function with 1 method) julia> fold = Foo(1) Foo(1) julia> struct Foo a::Int b::Int end julia> g() ERROR: MethodError: no method matching Foo(::Int64) The type `Foo` exists, but no method is defined for this combination of argument types when trying to construct it. Closest candidates are: Foo(::Int64, ::Int64) @ Main REPL[6]:2 Foo(::Any, ::Any) @ Main REPL[6]:2 Stacktrace: [1] g() @ Main ./REPL[2]:1 [2] top-level scope @ REPL[7]:1 julia> f(::Foo) = 2 f (generic function with 2 methods) julia> methods(f) [1] f(::Foo) @ REPL[8]:1 [2] f(::@world(Foo, 0:26898)) @ REPL[4]:1 julia> fold @world(Foo, 0:26898)(1) ``` 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. - [ ] Do we want to change the resolution time of bindings to (semantically) resolve them immediately? - [ ] Do we want to introduce guard bindings when inference assumes the absence of a binding? - [ ] Precompile re-validation - [ ] Various cleanups in the accessors - [ ] Invert the order of the binding linked list to make the most recent one always the head of the list - [ ] CodeInstances need forward edges for GlobalRefs not part of the uninferred code - [ ] Generated function support
- Loading branch information
Showing
25 changed files
with
527 additions
and
59 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,161 @@ | ||
# GlobalRef/binding reflection | ||
# TODO: This should potentially go in reflection.jl, but `@atomic` is not available | ||
# there. | ||
struct GlobalRefIterator | ||
mod::Module | ||
end | ||
globalrefs(mod::Module) = GlobalRefIterator(mod) | ||
|
||
function iterate(gri::GlobalRefIterator, i = 1) | ||
m = gri.mod | ||
table = ccall(:jl_module_get_bindings, Ref{SimpleVector}, (Any,), m) | ||
i == length(table) && return nothing | ||
b = table[i] | ||
b === nothing && return iterate(gri, i+1) | ||
return ((b::Core.Binding).globalref, i+1) | ||
end | ||
|
||
const TYPE_TYPE_MT = Type.body.name.mt | ||
const NONFUNCTION_MT = MethodTable.name.mt | ||
function foreach_module_mtable(visit, m::Module) | ||
for gb in globalrefs(m) | ||
binding = gb.binding | ||
if isconst(binding) | ||
isdefined(binding, :value) || continue | ||
v = @atomic binding.value | ||
uw = unwrap_unionall(v) | ||
name = gb.name | ||
if isa(uw, DataType) | ||
tn = uw.name | ||
if tn.module === m && tn.name === name && tn.wrapper === v && isdefined(tn, :mt) | ||
# this is the original/primary binding for the type (name/wrapper) | ||
mt = tn.mt | ||
if mt !== nothing && mt !== TYPE_TYPE_MT && mt !== NONFUNCTION_MT | ||
@assert mt.module === m | ||
visit(mt) || return false | ||
end | ||
end | ||
elseif isa(v, Module) && v !== m && parentmodule(v) === m && _nameof(v) === name | ||
# this is the original/primary binding for the submodule | ||
foreach_module_mtable(visit, v) || return false | ||
elseif isa(v, MethodTable) && v.module === m && v.name === name | ||
# this is probably an external method table here, so let's | ||
# assume so as there is no way to precisely distinguish them | ||
visit(v) || return false | ||
end | ||
end | ||
end | ||
return true | ||
end | ||
|
||
function foreach_reachable_mtable(visit) | ||
visit(TYPE_TYPE_MT) || return | ||
visit(NONFUNCTION_MT) || return | ||
if isdefined(Core.Main, :Base) | ||
for mod in Core.Main.Base.loaded_modules_array() | ||
foreach_module_mtable(visit, mod) | ||
end | ||
else | ||
foreach_module_mtable(visit, Core) | ||
foreach_module_mtable(visit, Core.Main) | ||
end | ||
end | ||
|
||
function invalidate_code_for_globalref!(gr::GlobalRef, src::CodeInfo) | ||
found_any = false | ||
labelchangemap = nothing | ||
stmts = src.code | ||
function get_labelchangemap() | ||
if labelchangemap === nothing | ||
labelchangemap = fill(0, length(stmts)) | ||
end | ||
labelchangemap | ||
end | ||
isgr(g::GlobalRef) = gr.mod == g.mod && gr.name === g.name | ||
isgr(g) = false | ||
for i = 1:length(stmts) | ||
stmt = stmts[i] | ||
if isgr(stmt) | ||
found_any = true | ||
continue | ||
end | ||
found_arg = false | ||
ngrs = 0 | ||
for ur in userefs(stmt) | ||
arg = ur[] | ||
# If any of the GlobalRefs in this stmt match the one that | ||
# we are about, we need to move out all GlobalRefs to preseve | ||
# effect order, in case we later invalidate a different GR | ||
if isa(arg, GlobalRef) | ||
ngrs += 1 | ||
if isgr(arg) | ||
@assert !isa(stmt, PhiNode) | ||
found_arg = found_any = true | ||
break | ||
end | ||
end | ||
end | ||
if found_arg | ||
get_labelchangemap()[i] += ngrs | ||
end | ||
end | ||
next_empty_idx = 1 | ||
if labelchangemap !== nothing | ||
cumsum_ssamap!(labelchangemap) | ||
new_stmts = Vector(undef, length(stmts)+labelchangemap[end]) | ||
new_ssaflags = Vector{UInt32}(undef, length(new_stmts)) | ||
new_debuginfo = DebugInfoStream(nothing, src.debuginfo, length(new_stmts)) | ||
new_debuginfo.def = src.debuginfo.def | ||
for i = 1:length(stmts) | ||
stmt = stmts[i] | ||
urs = userefs(stmt) | ||
new_stmt_idx = i+labelchangemap[i] | ||
for ur in urs | ||
arg = ur[] | ||
if isa(arg, SSAValue) | ||
ur[] = SSAValue(arg.id + labelchangemap[arg.id]) | ||
elseif next_empty_idx != new_stmt_idx && isa(arg, GlobalRef) | ||
new_debuginfo.codelocs[3next_empty_idx - 2] = i | ||
new_stmts[next_empty_idx] = arg | ||
new_ssaflags[next_empty_idx] = UInt32(0) | ||
ur[] = SSAValue(next_empty_idx) | ||
next_empty_idx += 1 | ||
end | ||
end | ||
@assert new_stmt_idx == next_empty_idx | ||
new_stmts[new_stmt_idx] = urs[] | ||
new_debuginfo.codelocs[3new_stmt_idx - 2] = i | ||
new_ssaflags[new_stmt_idx] = src.ssaflags[i] | ||
next_empty_idx = new_stmt_idx+1 | ||
end | ||
src.code = new_stmts | ||
src.ssavaluetypes = length(new_stmts) | ||
src.ssaflags = new_ssaflags | ||
src.debuginfo = Core.DebugInfo(new_debuginfo, length(new_stmts)) | ||
end | ||
return found_any | ||
end | ||
|
||
function invalidate_code_for_globalref!(gr::GlobalRef, new_max_world::UInt) | ||
valid_in_valuepos = false | ||
foreach_reachable_mtable() do mt::MethodTable | ||
for method in MethodList(mt) | ||
if isdefined(method, :source) | ||
src = _uncompressed_ir(method) | ||
old_stmts = src.code | ||
if invalidate_code_for_globalref!(gr, src) | ||
if src.code !== old_stmts | ||
method.debuginfo = src.debuginfo | ||
method.source = src | ||
method.source = ccall(:jl_compress_ir, Ref{String}, (Any, Ptr{Cvoid}), method, C_NULL) | ||
end | ||
|
||
for mi in specializations(method) | ||
ccall(:jl_invalidate_method_instance, Cvoid, (Any, UInt), mi, new_max_world) | ||
end | ||
end | ||
end | ||
end | ||
return true | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -810,6 +810,7 @@ export | |
@invoke, | ||
invokelatest, | ||
@invokelatest, | ||
@world, | ||
|
||
# loading source files | ||
__precompile__, | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.