Skip to content
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

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions base/Base.jl
Original file line number Diff line number Diff line change
Expand Up @@ -550,6 +550,9 @@ for m in methods(include)
delete_method(m)
end

# Arm binding invalidation mechanism
const invalidate_code_for_globalref! = Core.Compiler.invalidate_code_for_globalref!

# This method is here only to be overwritten during the test suite to test
# various sysimg related invalidation scenarios.
a_method_to_overwrite_in_test() = inferencebarrier(1)
Expand Down
2 changes: 0 additions & 2 deletions base/boot.jl
Original file line number Diff line number Diff line change
Expand Up @@ -541,8 +541,6 @@ GenericMemoryRef(mem::GenericMemory) = memoryref(mem)
GenericMemoryRef(mem::GenericMemory, i::Integer) = memoryref(mem, i)
GenericMemoryRef(mem::GenericMemoryRef, i::Integer) = memoryref(mem, i)

const Memory{T} = GenericMemory{:not_atomic, T, CPU}
const MemoryRef{T} = GenericMemoryRef{:not_atomic, T, CPU}
const AtomicMemory{T} = GenericMemory{:atomic, T, CPU}
const AtomicMemoryRef{T} = GenericMemoryRef{:atomic, T, CPU}

Expand Down
36 changes: 32 additions & 4 deletions base/compiler/abstractinterpretation.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2826,6 +2826,7 @@ end
isdefined_globalref(g::GlobalRef) = !iszero(ccall(:jl_globalref_boundp, Cint, (Any,), g))
isdefinedconst_globalref(g::GlobalRef) = isconst(g) && isdefined_globalref(g)

# TODO: This should verify that there is only one binding for this globalref
function abstract_eval_globalref_type(g::GlobalRef)
if isdefinedconst_globalref(g)
return Const(ccall(:jl_get_globalref_value, Any, (Any,), g))
Expand All @@ -2834,10 +2835,37 @@ function abstract_eval_globalref_type(g::GlobalRef)
ty === nothing && return Any
return ty
end
abstract_eval_global(M::Module, s::Symbol) = abstract_eval_globalref_type(GlobalRef(M, s))

function abstract_eval_binding_type(b::Core.Binding)
if isdefined(b, :owner)
b = b.owner
end
if isconst(b) && isdefined(b, :value)
return Const(b.value)
end
isdefined(b, :ty) || return Any
ty = b.ty
ty === nothing && return Any
return ty
end
function abstract_eval_global(M::Module, s::Symbol)
# TODO: This needs to add a new globalref to globalref edges list
return abstract_eval_globalref_type(GlobalRef(M, s))
end

function lookup_binding(world::UInt, g::GlobalRef)
ccall(:jl_lookup_module_binding, Any, (Any, Any, UInt), g.mod, g.name, world)::Union{Core.Binding, Nothing}
end

function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv::AbsIntState)
rt = abstract_eval_globalref_type(g)
binding = lookup_binding(get_inference_world(interp), g)
if binding === nothing
# TODO: We could allocate a guard entry here, but that would require
# going through a binding replacement if the binding ends up being used.
return RTEffects(Any, UndefVarError, Effects(EFFECTS_TOTAL; consistent=ALWAYS_FALSE, nothrow=false, inaccessiblememonly=ALWAYS_FALSE))
end
update_valid_age!(sv, WorldRange(binding.min_world, binding.max_world))
rt = abstract_eval_binding_type(binding)
consistent = inaccessiblememonly = ALWAYS_FALSE
nothrow = false
if isa(rt, Const)
Expand All @@ -2848,12 +2876,12 @@ function abstract_eval_globalref(interp::AbstractInterpreter, g::GlobalRef, sv::
end
elseif InferenceParams(interp).assume_bindings_static
consistent = inaccessiblememonly = ALWAYS_TRUE
if isdefined_globalref(g)
if isdefined(binding, :value)
nothrow = true
else
rt = Union{}
end
elseif isdefinedconst_globalref(g)
elseif isdefined(binding, :value) && isconst(binding)
nothrow = true
end
return RTEffects(rt, nothrow ? Union{} : UndefVarError, Effects(EFFECTS_TOTAL; consistent, nothrow, inaccessiblememonly))
Expand Down
2 changes: 2 additions & 0 deletions base/compiler/compiler.jl
Original file line number Diff line number Diff line change
Expand Up @@ -222,5 +222,7 @@ ccall(:jl_set_typeinf_func, Cvoid, (Any,), typeinf_ext_toplevel)
include("compiler/parsing.jl")
Core._setparser!(fl_parse)

include("compiler/invalidation.jl")

end # baremodule Compiler
))
161 changes: 161 additions & 0 deletions base/compiler/invalidation.jl
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

Check warning on line 87 in base/compiler/invalidation.jl

View workflow job for this annotation

GitHub Actions / Check for new typos

perhaps "preseve" should be "preserve".
# 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
44 changes: 44 additions & 0 deletions base/essentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1069,6 +1069,50 @@ function invoke_in_world(world::UInt, @nospecialize(f), @nospecialize args...; k
return Core._call_in_world(world, Core.kwcall, kwargs, f, args...)
end

"""
@world(sym, world)

Resolve the binding `sym` in world `world`. See [`invoke_in_world`](@ref) for running
arbitrary code in fixed worlds. `world` may be `UnitRange`, in which case the macro
will error unless the binding is valid and has the same value across the entire world
range.

The `@world` macro is primarily used in the priniting of bindings that are no longer available
in the current world.

## Example
```
julia> struct Foo; a::Int; end
Foo

julia> fold = Foo(1)

julia> Int(Base.get_world_counter())
26866

julia> struct Foo; a::Int; b::Int end
Foo

julia> fold
@world(Foo, 26866)(1)
```

!!! compat "Julia 1.12"
This functionality requires at least Julia 1.12.
"""
macro world(sym, world)
if isa(sym, Symbol)
return :($(_resolve_in_world)($world, $(QuoteNode(GlobalRef(__module__, sym)))))
elseif isa(sym, GlobalRef)
return :($(_resolve_in_world)($world, $(QuoteNode(sym))))
else
error("`@world` requires a symbol or GlobalRef")
end
end

_resolve_in_world(world::Integer, gr::GlobalRef) =
invoke_in_world(UInt(world), Core.getglobal, gr.mod, gr.name)

inferencebarrier(@nospecialize(x)) = compilerbarrier(:type, x)

"""
Expand Down
1 change: 1 addition & 0 deletions base/exports.jl
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,7 @@ export
@invoke,
invokelatest,
@invokelatest,
@world,

# loading source files
__precompile__,
Expand Down
13 changes: 13 additions & 0 deletions base/range.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1680,3 +1680,16 @@ function show(io::IO, r::LogRange{T}) where {T}
show(io, length(r))
print(io, ')')
end

# Implementation detail of @world
# The rest of this is defined in essentials.jl, but UnitRange is not available
function _resolve_in_world(world::UnitRange, gr::GlobalRef)
# Validate that this binding's reference covers the entire world range
bnd = ccall(:jl_lookup_module_binding, Any, (Any, Any, UInt), gr.mod, gr.name, first(world))::Union{Core.Binding, Nothing}
if bnd !== nothing
if bnd.max_world < last(world)
error("Binding does not cover the full world range")
end
end
_resolve_in_world(last(world), gr)
end
15 changes: 15 additions & 0 deletions base/reflection.jl
Original file line number Diff line number Diff line change
Expand Up @@ -344,6 +344,9 @@ function isconst(g::GlobalRef)
return ccall(:jl_globalref_is_const, Cint, (Any,), g) != 0
end

isconst(b::Core.Binding) =
ccall(:jl_binding_is_const, Cint, (Any,), b) != 0

"""
isconst(t::DataType, s::Union{Int,Symbol}) -> Bool

Expand Down Expand Up @@ -2595,6 +2598,18 @@ function delete_method(m::Method)
ccall(:jl_method_table_disable, Cvoid, (Any, Any), get_methodtable(m), m)
end

"""
delete_binding(mod::Module, sym::Symbol)

Force the binding `mod.sym` to be undefined again, allowing it be redefined.
Note that this operation is very expensive, requirinig a full scan of all code in the system,
as well as potential recompilation of any methods that (may) have used binding
information.
"""
function delete_binding(mod::Module, sym::Symbol)
ccall(:jl_disable_binding, Cvoid, (Any,), GlobalRef(mod, sym))
end

function get_methodtable(m::Method)
mt = ccall(:jl_method_get_table, Any, (Any,), m)
if mt === nothing
Expand Down
21 changes: 21 additions & 0 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1040,6 +1040,24 @@ function is_global_function(tn::Core.TypeName, globname::Union{Symbol,Nothing})
return false
end

function check_world_bounded(tn)
bnd = ccall(:jl_get_module_binding, Any, (Any, Any, Cint, UInt), tn.module, tn.name, false, 1)::Core.Binding
if bnd !== nothing
while true
if isdefined(bnd, :owner) && isdefined(bnd, :value)
if bnd.value <: tn.wrapper
max_world = @atomic bnd.max_world
max_world == typemax(UInt) && return nothing
return Int(bnd.min_world):Int(max_world)
end
end
isdefined(bnd, :next) || break
bnd = @atomic bnd.next
end
end
return nothing
end

function show_type_name(io::IO, tn::Core.TypeName)
if tn === UnionAll.name
# by coincidence, `typeof(Type)` is a valid representation of the UnionAll type.
Expand Down Expand Up @@ -1068,7 +1086,10 @@ function show_type_name(io::IO, tn::Core.TypeName)
end
end
end
world = check_world_bounded(tn)
world !== nothing && print(io, "@world(")
show_sym(io, sym)
world !== nothing && print(io, ", ", world, ")")
quo && print(io, ")")
globfunc && print(io, ")")
nothing
Expand Down
Loading
Loading