Skip to content

Commit 52b3e82

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. # Motivation 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. # Implementation 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. # Demo ``` 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) # 2 methods for generic function "f" from Main: [1] f(::Foo) @ REPL[8]:1 [2] f(::@world(Foo, 0:26898)) @ REPL[4]:1 julia> fold @world(Foo, 0:26898)(1) ``` # 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 - [ ] 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? # Implementation TODO - [ ] 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 dc63ab2 commit 52b3e82

25 files changed

+524
-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
@@ -544,8 +544,6 @@ GenericMemoryRef{kind,T,AS}(mem::GenericMemory{kind,T,AS}) where {kind,T,AS} =
544544
GenericMemoryRef{kind,T,AS}(ref::GenericMemoryRef{kind,T,AS}, i::Integer) where {kind,T,AS} = memoryref(ref, Int(i), @_boundscheck)
545545
GenericMemoryRef{kind,T,AS}(mem::GenericMemory{kind,T,AS}, i::Integer) where {kind,T,AS} = memoryref(memoryref(mem), Int(i), @_boundscheck)
546546

547-
const Memory{T} = GenericMemory{:not_atomic, T, CPU}
548-
const MemoryRef{T} = GenericMemoryRef{:not_atomic, T, CPU}
549547
const AtomicMemory{T} = GenericMemory{:atomic, T, CPU}
550548
const AtomicMemoryRef{T} = GenericMemoryRef{:atomic, T, CPU}
551549

base/compiler/abstractinterpretation.jl

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

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

28352856
function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv::AbsIntState)
2836-
rt = abstract_eval_globalref_type(g)
2857+
binding = lookup_binding(get_inference_world(interp), g)
2858+
if binding === nothing
2859+
# TODO: We could allocate a guard entry here, but that would require
2860+
# going through a binding replacement if the binding ends up being used.
2861+
return RTEffects(Any, UndefVarError, Effects(EFFECTS_TOTAL; consistent=ALWAYS_FALSE, nothrow=false, inaccessiblememonly=ALWAYS_FALSE))
2862+
end
2863+
update_valid_age!(sv, WorldRange(binding.min_world, binding.max_world))
2864+
rt = abstract_eval_binding_type(binding)
28372865
consistent = inaccessiblememonly = ALWAYS_FALSE
28382866
nothrow = false
28392867
if isa(rt, Const)
@@ -2844,12 +2872,12 @@ function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv::
28442872
end
28452873
elseif InferenceParams(interp).assume_bindings_static
28462874
consistent = inaccessiblememonly = ALWAYS_TRUE
2847-
if isdefined_globalref(g)
2875+
if isdefined(binding, :value)
28482876
nothrow = true
28492877
else
28502878
rt = Union{}
28512879
end
2852-
elseif isdefinedconst_globalref(g)
2880+
elseif isdefined(binding, :value) && isconst(binding)
28532881
nothrow = true
28542882
end
28552883
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
@@ -809,6 +809,7 @@ export
809809
@invoke,
810810
invokelatest,
811811
@invokelatest,
812+
@world,
812813

813814
# loading source files
814815
__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
@@ -339,6 +339,9 @@ function isconst(g::GlobalRef)
339339
return ccall(:jl_globalref_is_const, Cint, (Any,), g) != 0
340340
end
341341

342+
isconst(b::Core.Binding) =
343+
ccall(:jl_binding_is_const, Cint, (Any,), b) != 0
344+
342345
"""
343346
isconst(t::DataType, s::Union{Int,Symbol}) -> Bool
344347
@@ -2585,6 +2588,18 @@ function delete_method(m::Method)
25852588
ccall(:jl_method_table_disable, Cvoid, (Any, Any), get_methodtable(m), m)
25862589
end
25872590

2591+
"""
2592+
delete_binding(mod::Module, sym::Symbol)
2593+
2594+
Force the binding `mod.sym` to be undefined again, allowing it be redefined.
2595+
Note that this operation is very expensive, requirinig a full scan of all code in the system,
2596+
as well as potential recompilation of any methods that (may) have used binding
2597+
information.
2598+
"""
2599+
function delete_binding(mod::Module, sym::Symbol)
2600+
ccall(:jl_disable_binding, Cvoid, (Any,), GlobalRef(mod, sym))
2601+
end
2602+
25882603
function get_methodtable(m::Method)
25892604
mt = ccall(:jl_method_get_table, Any, (Any,), m)
25902605
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)