Skip to content

Commit 06e6939

Browse files
committed
WIP: World-age parition bindings
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
1 parent 77c28ab commit 06e6939

25 files changed

+527
-59
lines changed

base/Base.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -550,6 +550,9 @@ for m in methods(include)
550550
delete_method(m)
551551
end
552552

553+
# Arm binding invalidation mechanism
554+
const invalidate_code_for_globalref! = Core.Compiler.invalidate_code_for_globalref!
555+
553556
# This method is here only to be overwritten during the test suite to test
554557
# various sysimg related invalidation scenarios.
555558
a_method_to_overwrite_in_test() = inferencebarrier(1)

base/boot.jl

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -541,8 +541,6 @@ GenericMemoryRef(mem::GenericMemory) = memoryref(mem)
541541
GenericMemoryRef(mem::GenericMemory, i::Integer) = memoryref(mem, i)
542542
GenericMemoryRef(mem::GenericMemoryRef, i::Integer) = memoryref(mem, i)
543543

544-
const Memory{T} = GenericMemory{:not_atomic, T, CPU}
545-
const MemoryRef{T} = GenericMemoryRef{:not_atomic, T, CPU}
546544
const AtomicMemory{T} = GenericMemory{:atomic, T, CPU}
547545
const AtomicMemoryRef{T} = GenericMemoryRef{:atomic, T, CPU}
548546

base/compiler/abstractinterpretation.jl

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2826,6 +2826,7 @@ end
28262826
isdefined_globalref(g::GlobalRef) = !iszero(ccall(:jl_globalref_boundp, Cint, (Any,), g))
28272827
isdefinedconst_globalref(g::GlobalRef) = isconst(g) && isdefined_globalref(g)
28282828

2829+
# TODO: This should verify that there is only one binding for this globalref
28292830
function abstract_eval_globalref_type(g::GlobalRef)
28302831
if isdefinedconst_globalref(g)
28312832
return Const(ccall(:jl_get_globalref_value, Any, (Any,), g))
@@ -2834,10 +2835,37 @@ function abstract_eval_globalref_type(g::GlobalRef)
28342835
ty === nothing && return Any
28352836
return ty
28362837
end
2837-
abstract_eval_global(M::Module, s::Symbol) = abstract_eval_globalref_type(GlobalRef(M, s))
2838+
2839+
function abstract_eval_binding_type(b::Core.Binding)
2840+
if isdefined(b, :owner)
2841+
b = b.owner
2842+
end
2843+
if isconst(b) && isdefined(b, :value)
2844+
return Const(b.value)
2845+
end
2846+
isdefined(b, :ty) || return Any
2847+
ty = b.ty
2848+
ty === nothing && return Any
2849+
return ty
2850+
end
2851+
function abstract_eval_global(M::Module, s::Symbol)
2852+
# TODO: This needs to add a new globalref to globalref edges list
2853+
return abstract_eval_globalref_type(GlobalRef(M, s))
2854+
end
2855+
2856+
function lookup_binding(world::UInt, g::GlobalRef)
2857+
ccall(:jl_lookup_module_binding, Any, (Any, Any, UInt), g.mod, g.name, world)::Union{Core.Binding, Nothing}
2858+
end
28382859

28392860
function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv::AbsIntState)
2840-
rt = abstract_eval_globalref_type(g)
2861+
binding = lookup_binding(get_inference_world(interp), g)
2862+
if binding === nothing
2863+
# TODO: We could allocate a guard entry here, but that would require
2864+
# going through a binding replacement if the binding ends up being used.
2865+
return RTEffects(Any, UndefVarError, Effects(EFFECTS_TOTAL; consistent=ALWAYS_FALSE, nothrow=false, inaccessiblememonly=ALWAYS_FALSE))
2866+
end
2867+
update_valid_age!(sv, WorldRange(binding.min_world, binding.max_world))
2868+
rt = abstract_eval_binding_type(binding)
28412869
consistent = inaccessiblememonly = ALWAYS_FALSE
28422870
nothrow = false
28432871
if isa(rt, Const)
@@ -2848,12 +2876,12 @@ function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv::
28482876
end
28492877
elseif InferenceParams(interp).assume_bindings_static
28502878
consistent = inaccessiblememonly = ALWAYS_TRUE
2851-
if isdefined_globalref(g)
2879+
if isdefined(binding, :value)
28522880
nothrow = true
28532881
else
28542882
rt = Union{}
28552883
end
2856-
elseif isdefinedconst_globalref(g)
2884+
elseif isdefined(binding, :value) && isconst(binding)
28572885
nothrow = true
28582886
end
28592887
return RTEffects(rt, nothrow ? Union{} : UndefVarError, Effects(EFFECTS_TOTAL; consistent, nothrow, inaccessiblememonly))

base/compiler/compiler.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,5 +222,7 @@ ccall(:jl_set_typeinf_func, Cvoid, (Any,), typeinf_ext_toplevel)
222222
include("compiler/parsing.jl")
223223
Core._setparser!(fl_parse)
224224

225+
include("compiler/invalidation.jl")
226+
225227
end # baremodule Compiler
226228
))

base/compiler/invalidation.jl

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# GlobalRef/binding reflection
2+
# TODO: This should potentially go in reflection.jl, but `@atomic` is not available
3+
# there.
4+
struct GlobalRefIterator
5+
mod::Module
6+
end
7+
globalrefs(mod::Module) = GlobalRefIterator(mod)
8+
9+
function iterate(gri::GlobalRefIterator, i = 1)
10+
m = gri.mod
11+
table = ccall(:jl_module_get_bindings, Ref{SimpleVector}, (Any,), m)
12+
i == length(table) && return nothing
13+
b = table[i]
14+
b === nothing && return iterate(gri, i+1)
15+
return ((b::Core.Binding).globalref, i+1)
16+
end
17+
18+
const TYPE_TYPE_MT = Type.body.name.mt
19+
const NONFUNCTION_MT = MethodTable.name.mt
20+
function foreach_module_mtable(visit, m::Module)
21+
for gb in globalrefs(m)
22+
binding = gb.binding
23+
if isconst(binding)
24+
isdefined(binding, :value) || continue
25+
v = @atomic binding.value
26+
uw = unwrap_unionall(v)
27+
name = gb.name
28+
if isa(uw, DataType)
29+
tn = uw.name
30+
if tn.module === m && tn.name === name && tn.wrapper === v && isdefined(tn, :mt)
31+
# this is the original/primary binding for the type (name/wrapper)
32+
mt = tn.mt
33+
if mt !== nothing && mt !== TYPE_TYPE_MT && mt !== NONFUNCTION_MT
34+
@assert mt.module === m
35+
visit(mt) || return false
36+
end
37+
end
38+
elseif isa(v, Module) && v !== m && parentmodule(v) === m && _nameof(v) === name
39+
# this is the original/primary binding for the submodule
40+
foreach_module_mtable(visit, v) || return false
41+
elseif isa(v, MethodTable) && v.module === m && v.name === name
42+
# this is probably an external method table here, so let's
43+
# assume so as there is no way to precisely distinguish them
44+
visit(v) || return false
45+
end
46+
end
47+
end
48+
return true
49+
end
50+
51+
function foreach_reachable_mtable(visit)
52+
visit(TYPE_TYPE_MT) || return
53+
visit(NONFUNCTION_MT) || return
54+
if isdefined(Core.Main, :Base)
55+
for mod in Core.Main.Base.loaded_modules_array()
56+
foreach_module_mtable(visit, mod)
57+
end
58+
else
59+
foreach_module_mtable(visit, Core)
60+
foreach_module_mtable(visit, Core.Main)
61+
end
62+
end
63+
64+
function invalidate_code_for_globalref!(gr::GlobalRef, src::CodeInfo)
65+
found_any = false
66+
labelchangemap = nothing
67+
stmts = src.code
68+
function get_labelchangemap()
69+
if labelchangemap === nothing
70+
labelchangemap = fill(0, length(stmts))
71+
end
72+
labelchangemap
73+
end
74+
isgr(g::GlobalRef) = gr.mod == g.mod && gr.name === g.name
75+
isgr(g) = false
76+
for i = 1:length(stmts)
77+
stmt = stmts[i]
78+
if isgr(stmt)
79+
found_any = true
80+
continue
81+
end
82+
found_arg = false
83+
ngrs = 0
84+
for ur in userefs(stmt)
85+
arg = ur[]
86+
# If any of the GlobalRefs in this stmt match the one that
87+
# we are about, we need to move out all GlobalRefs to preseve
88+
# effect order, in case we later invalidate a different GR
89+
if isa(arg, GlobalRef)
90+
ngrs += 1
91+
if isgr(arg)
92+
@assert !isa(stmt, PhiNode)
93+
found_arg = found_any = true
94+
break
95+
end
96+
end
97+
end
98+
if found_arg
99+
get_labelchangemap()[i] += ngrs
100+
end
101+
end
102+
next_empty_idx = 1
103+
if labelchangemap !== nothing
104+
cumsum_ssamap!(labelchangemap)
105+
new_stmts = Vector(undef, length(stmts)+labelchangemap[end])
106+
new_ssaflags = Vector{UInt32}(undef, length(new_stmts))
107+
new_debuginfo = DebugInfoStream(nothing, src.debuginfo, length(new_stmts))
108+
new_debuginfo.def = src.debuginfo.def
109+
for i = 1:length(stmts)
110+
stmt = stmts[i]
111+
urs = userefs(stmt)
112+
new_stmt_idx = i+labelchangemap[i]
113+
for ur in urs
114+
arg = ur[]
115+
if isa(arg, SSAValue)
116+
ur[] = SSAValue(arg.id + labelchangemap[arg.id])
117+
elseif next_empty_idx != new_stmt_idx && isa(arg, GlobalRef)
118+
new_debuginfo.codelocs[3next_empty_idx - 2] = i
119+
new_stmts[next_empty_idx] = arg
120+
new_ssaflags[next_empty_idx] = UInt32(0)
121+
ur[] = SSAValue(next_empty_idx)
122+
next_empty_idx += 1
123+
end
124+
end
125+
@assert new_stmt_idx == next_empty_idx
126+
new_stmts[new_stmt_idx] = urs[]
127+
new_debuginfo.codelocs[3new_stmt_idx - 2] = i
128+
new_ssaflags[new_stmt_idx] = src.ssaflags[i]
129+
next_empty_idx = new_stmt_idx+1
130+
end
131+
src.code = new_stmts
132+
src.ssavaluetypes = length(new_stmts)
133+
src.ssaflags = new_ssaflags
134+
src.debuginfo = Core.DebugInfo(new_debuginfo, length(new_stmts))
135+
end
136+
return found_any
137+
end
138+
139+
function invalidate_code_for_globalref!(gr::GlobalRef, new_max_world::UInt)
140+
valid_in_valuepos = false
141+
foreach_reachable_mtable() do mt::MethodTable
142+
for method in MethodList(mt)
143+
if isdefined(method, :source)
144+
src = _uncompressed_ir(method)
145+
old_stmts = src.code
146+
if invalidate_code_for_globalref!(gr, src)
147+
if src.code !== old_stmts
148+
method.debuginfo = src.debuginfo
149+
method.source = src
150+
method.source = ccall(:jl_compress_ir, Ref{String}, (Any, Ptr{Cvoid}), method, C_NULL)
151+
end
152+
153+
for mi in specializations(method)
154+
ccall(:jl_invalidate_method_instance, Cvoid, (Any, UInt), mi, new_max_world)
155+
end
156+
end
157+
end
158+
end
159+
return true
160+
end
161+
end

base/essentials.jl

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,6 +1069,50 @@ function invoke_in_world(world::UInt, @nospecialize(f), @nospecialize args...; k
10691069
return Core._call_in_world(world, Core.kwcall, kwargs, f, args...)
10701070
end
10711071

1072+
"""
1073+
@world(sym, world)
1074+
1075+
Resolve the binding `sym` in world `world`. See [`invoke_in_world`](@ref) for running
1076+
arbitrary code in fixed worlds. `world` may be `UnitRange`, in which case the macro
1077+
will error unless the binding is valid and has the same value across the entire world
1078+
range.
1079+
1080+
The `@world` macro is primarily used in the priniting of bindings that are no longer available
1081+
in the current world.
1082+
1083+
## Example
1084+
```
1085+
julia> struct Foo; a::Int; end
1086+
Foo
1087+
1088+
julia> fold = Foo(1)
1089+
1090+
julia> Int(Base.get_world_counter())
1091+
26866
1092+
1093+
julia> struct Foo; a::Int; b::Int end
1094+
Foo
1095+
1096+
julia> fold
1097+
@world(Foo, 26866)(1)
1098+
```
1099+
1100+
!!! compat "Julia 1.12"
1101+
This functionality requires at least Julia 1.12.
1102+
"""
1103+
macro world(sym, world)
1104+
if isa(sym, Symbol)
1105+
return :($(_resolve_in_world)($world, $(QuoteNode(GlobalRef(__module__, sym)))))
1106+
elseif isa(sym, GlobalRef)
1107+
return :($(_resolve_in_world)($world, $(QuoteNode(sym))))
1108+
else
1109+
error("`@world` requires a symbol or GlobalRef")
1110+
end
1111+
end
1112+
1113+
_resolve_in_world(world::Integer, gr::GlobalRef) =
1114+
invoke_in_world(UInt(world), Core.getglobal, gr.mod, gr.name)
1115+
10721116
inferencebarrier(@nospecialize(x)) = compilerbarrier(:type, x)
10731117

10741118
"""

base/exports.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,7 @@ export
810810
@invoke,
811811
invokelatest,
812812
@invokelatest,
813+
@world,
813814

814815
# loading source files
815816
__precompile__,

base/range.jl

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1680,3 +1680,16 @@ function show(io::IO, r::LogRange{T}) where {T}
16801680
show(io, length(r))
16811681
print(io, ')')
16821682
end
1683+
1684+
# Implementation detail of @world
1685+
# The rest of this is defined in essentials.jl, but UnitRange is not available
1686+
function _resolve_in_world(world::UnitRange, gr::GlobalRef)
1687+
# Validate that this binding's reference covers the entire world range
1688+
bnd = ccall(:jl_lookup_module_binding, Any, (Any, Any, UInt), gr.mod, gr.name, first(world))::Union{Core.Binding, Nothing}
1689+
if bnd !== nothing
1690+
if bnd.max_world < last(world)
1691+
error("Binding does not cover the full world range")
1692+
end
1693+
end
1694+
_resolve_in_world(last(world), gr)
1695+
end

base/reflection.jl

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,9 @@ function isconst(g::GlobalRef)
344344
return ccall(:jl_globalref_is_const, Cint, (Any,), g) != 0
345345
end
346346

347+
isconst(b::Core.Binding) =
348+
ccall(:jl_binding_is_const, Cint, (Any,), b) != 0
349+
347350
"""
348351
isconst(t::DataType, s::Union{Int,Symbol}) -> Bool
349352
@@ -2595,6 +2598,18 @@ function delete_method(m::Method)
25952598
ccall(:jl_method_table_disable, Cvoid, (Any, Any), get_methodtable(m), m)
25962599
end
25972600

2601+
"""
2602+
delete_binding(mod::Module, sym::Symbol)
2603+
2604+
Force the binding `mod.sym` to be undefined again, allowing it be redefined.
2605+
Note that this operation is very expensive, requirinig a full scan of all code in the system,
2606+
as well as potential recompilation of any methods that (may) have used binding
2607+
information.
2608+
"""
2609+
function delete_binding(mod::Module, sym::Symbol)
2610+
ccall(:jl_disable_binding, Cvoid, (Any,), GlobalRef(mod, sym))
2611+
end
2612+
25982613
function get_methodtable(m::Method)
25992614
mt = ccall(:jl_method_get_table, Any, (Any,), m)
26002615
if mt === nothing

base/show.jl

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1040,6 +1040,24 @@ function is_global_function(tn::Core.TypeName, globname::Union{Symbol,Nothing})
10401040
return false
10411041
end
10421042

1043+
function check_world_bounded(tn)
1044+
bnd = ccall(:jl_get_module_binding, Any, (Any, Any, Cint, UInt), tn.module, tn.name, false, 1)::Core.Binding
1045+
if bnd !== nothing
1046+
while true
1047+
if isdefined(bnd, :owner) && isdefined(bnd, :value)
1048+
if bnd.value <: tn.wrapper
1049+
max_world = @atomic bnd.max_world
1050+
max_world == typemax(UInt) && return nothing
1051+
return Int(bnd.min_world):Int(max_world)
1052+
end
1053+
end
1054+
isdefined(bnd, :next) || break
1055+
bnd = @atomic bnd.next
1056+
end
1057+
end
1058+
return nothing
1059+
end
1060+
10431061
function show_type_name(io::IO, tn::Core.TypeName)
10441062
if tn === UnionAll.name
10451063
# by coincidence, `typeof(Type)` is a valid representation of the UnionAll type.
@@ -1068,7 +1086,10 @@ function show_type_name(io::IO, tn::Core.TypeName)
10681086
end
10691087
end
10701088
end
1089+
world = check_world_bounded(tn)
1090+
world !== nothing && print(io, "@world(")
10711091
show_sym(io, sym)
1092+
world !== nothing && print(io, ", ", world, ")")
10721093
quo && print(io, ")")
10731094
globfunc && print(io, ")")
10741095
nothing

0 commit comments

Comments
 (0)