Skip to content

Add basic infrastructure for binding replacement #56224

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

Merged
merged 1 commit into from
Oct 21, 2024
Merged
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
44 changes: 44 additions & 0 deletions base/essentials.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1250,6 +1250,50 @@ function isiterable(T)::Bool
return hasmethod(iterate, Tuple{T})
end

"""
@world(sym, world)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we attempt to merge this with Base.@invokelatest world M.a?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@IanButterworth suggested that above. The syntax is reserved, but I'm not convinced.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not convinced either, just observing the symmetry that the two different macros return the same result, particularly if we allow @world to handle call syntax or @invokelatest to handle bindings


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)

# Special constprop heuristics for various binary opes
typename(typeof(function + end)).constprop_heuristic = Core.SAMETYPE_HEURISTIC
typename(typeof(function - end)).constprop_heuristic = Core.SAMETYPE_HEURISTIC
Expand Down
11 changes: 11 additions & 0 deletions base/range.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1680,3 +1680,14 @@ 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(worlds::UnitRange, gr::GlobalRef)
# Validate that this binding's reference covers the entire world range
bpart = lookup_binding_partition(first(worlds), gr)
if bpart.max_world < last(world)
error("Binding does not cover the full world range")
end
_resolve_in_world(last(world), gr)
end
27 changes: 24 additions & 3 deletions base/runtime_internals.jl
Original file line number Diff line number Diff line change
Expand Up @@ -218,16 +218,19 @@ function _fieldnames(@nospecialize t)
return t.name.names
end

const BINDING_KIND_GLOBAL = 0x0
const BINDING_KIND_CONST = 0x1
const BINDING_KIND_CONST_IMPORT = 0x2
# N.B.: Needs to be synced with julia.h
const BINDING_KIND_CONST = 0x0
const BINDING_KIND_CONST_IMPORT = 0x1
const BINDING_KIND_GLOBAL = 0x2
const BINDING_KIND_IMPLICIT = 0x3
const BINDING_KIND_EXPLICIT = 0x4
const BINDING_KIND_IMPORTED = 0x5
const BINDING_KIND_FAILED = 0x6
const BINDING_KIND_DECLARED = 0x7
const BINDING_KIND_GUARD = 0x8

is_some_const_binding(kind::UInt8) = (kind == BINDING_KIND_CONST || kind == BINDING_KIND_CONST_IMPORT)

function lookup_binding_partition(world::UInt, b::Core.Binding)
ccall(:jl_get_binding_partition, Ref{Core.BindingPartition}, (Any, UInt), b, world)
end
Expand All @@ -236,9 +239,27 @@ function lookup_binding_partition(world::UInt, gr::Core.GlobalRef)
ccall(:jl_get_globalref_partition, Ref{Core.BindingPartition}, (Any, UInt), gr, world)
end

partition_restriction(bpart::Core.BindingPartition) = ccall(:jl_bpart_get_restriction_value, Any, (Any,), bpart)

binding_kind(bpart::Core.BindingPartition) = ccall(:jl_bpart_get_kind, UInt8, (Any,), bpart)
binding_kind(m::Module, s::Symbol) = binding_kind(lookup_binding_partition(tls_world_age(), GlobalRef(m, s)))

"""
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.

!!! warning
The implementation of this functionality is currently incomplete. Do not use
this method on versions that contain this disclaimer except for testing.
Comment on lines +255 to +257
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do add the --experimental flag, this seems like a probably a good candidate to add to it also?

"""
function delete_binding(mod::Module, sym::Symbol)
ccall(:jl_disable_binding, Cvoid, (Any,), GlobalRef(mod, sym))
end

"""
fieldname(x::DataType, i::Integer)

Expand Down
18 changes: 18 additions & 0 deletions base/show.jl
Original file line number Diff line number Diff line change
Expand Up @@ -1032,6 +1032,21 @@ function is_global_function(tn::Core.TypeName, globname::Union{Symbol,Nothing})
return false
end

function check_world_bounded(tn::Core.TypeName)
bnd = ccall(:jl_get_module_binding, Ref{Core.Binding}, (Any, Any, Cint), tn.module, tn.name, true)
isdefined(bnd, :partitions) || return nothing
partition = @atomic bnd.partitions
while true
if is_some_const_binding(binding_kind(partition)) && partition_restriction(partition) <: tn.wrapper
max_world = @atomic partition.max_world
max_world == typemax(UInt) && return nothing
return Int(partition.min_world):Int(max_world)
end
isdefined(partition, :next) || return nothing
partition = @atomic partition.next
end
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 @@ -1060,7 +1075,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
2 changes: 2 additions & 0 deletions src/clangsa/GCChecker.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -819,6 +819,7 @@ bool GCChecker::isGCTrackedType(QualType QT) {
Name.ends_with_insensitive("jl_tupletype_t") ||
Name.ends_with_insensitive("jl_gc_tracked_buffer_t") ||
Name.ends_with_insensitive("jl_binding_t") ||
Name.ends_with_insensitive("jl_binding_partition_t") ||
Name.ends_with_insensitive("jl_ordereddict_t") ||
Name.ends_with_insensitive("jl_tvar_t") ||
Name.ends_with_insensitive("jl_typemap_t") ||
Expand All @@ -842,6 +843,7 @@ bool GCChecker::isGCTrackedType(QualType QT) {
Name.ends_with_insensitive("jl_stenv_t") ||
Name.ends_with_insensitive("jl_varbinding_t") ||
Name.ends_with_insensitive("set_world") ||
Name.ends_with_insensitive("jl_ptr_kind_union_t") ||
Name.ends_with_insensitive("jl_codectx_t")) {
return true;
}
Expand Down
11 changes: 6 additions & 5 deletions src/julia.h
Original file line number Diff line number Diff line change
Expand Up @@ -620,6 +620,7 @@ typedef struct _jl_weakref_t {
jl_value_t *value;
} jl_weakref_t;

// N.B: Needs to be synced with runtime_internals.jl
enum jl_partition_kind {
// Constant: This binding partition is a constant declared using `const`
// ->restriction holds the constant value
Expand Down Expand Up @@ -684,7 +685,7 @@ typedef struct __attribute__((aligned(8))) _jl_binding_partition_t {
_Atomic(jl_ptr_kind_union_t) restriction;
size_t min_world;
_Atomic(size_t) max_world;
_Atomic(struct _jl_binding_partition_t*) next;
_Atomic(struct _jl_binding_partition_t *) next;
size_t reserved; // Reserved for ->kind. Currently this holds the low bits of ->restriction during serialization
} jl_binding_partition_t;

Expand Down Expand Up @@ -1839,8 +1840,8 @@ JL_DLLEXPORT jl_sym_t *jl_symbol_n(const char *str, size_t len) JL_NOTSAFEPOINT;
JL_DLLEXPORT jl_sym_t *jl_gensym(void);
JL_DLLEXPORT jl_sym_t *jl_tagged_gensym(const char *str, size_t len);
JL_DLLEXPORT jl_sym_t *jl_get_root_symbol(void);
JL_DLLEXPORT jl_value_t *jl_get_binding_value(jl_binding_t *b JL_PROPAGATES_ROOT) JL_NOTSAFEPOINT;
JL_DLLEXPORT jl_value_t *jl_get_binding_value_if_const(jl_binding_t *b JL_PROPAGATES_ROOT) JL_NOTSAFEPOINT;
JL_DLLEXPORT jl_value_t *jl_get_binding_value(jl_binding_t *b JL_PROPAGATES_ROOT);
JL_DLLEXPORT jl_value_t *jl_get_binding_value_if_const(jl_binding_t *b JL_PROPAGATES_ROOT);
Comment on lines +1843 to +1844
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these changes violate JL_NOTSAFEPOINT requirements in rtutils and codegen (the two files we aren't able to verify). I think you partly fixed one of the issues, but not the others

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I fixed rtutils. I don't think codegen assumes JL_NOTSAFEPOINT, but regardless these will be removed from codegen anyway as it gets adjusted to partition.

Copy link
Member

@vtjnash vtjnash Oct 31, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I saw the one place was fixed, but looks like jl_stderr_obj still needs to be deleted still from rtutils, as it is now invalid (and probably doesn't get much use, but its one use is now invalidated by removing the annotation). The annotations on jl_declare_constant_val also still need to be fixed, as it looks like we now violate them, and pass in unrooted objects (var) then call the allocation function

JL_DLLEXPORT jl_value_t *jl_declare_const_gf(jl_binding_t *b, jl_module_t *mod, jl_sym_t *name);
JL_DLLEXPORT jl_method_t *jl_method_def(jl_svec_t *argdata, jl_methtable_t *mt, jl_code_info_t *f, jl_module_t *module);
JL_DLLEXPORT jl_code_info_t *jl_code_for_staged(jl_method_instance_t *linfo, size_t world, jl_code_instance_t **cache);
Expand Down Expand Up @@ -2002,8 +2003,8 @@ JL_DLLEXPORT jl_value_t *jl_checked_swap(jl_binding_t *b, jl_module_t *mod, jl_s
JL_DLLEXPORT jl_value_t *jl_checked_replace(jl_binding_t *b, jl_module_t *mod, jl_sym_t *var, jl_value_t *expected, jl_value_t *rhs);
JL_DLLEXPORT jl_value_t *jl_checked_modify(jl_binding_t *b, jl_module_t *mod, jl_sym_t *var, jl_value_t *op, jl_value_t *rhs);
JL_DLLEXPORT jl_value_t *jl_checked_assignonce(jl_binding_t *b, jl_module_t *mod, jl_sym_t *var, jl_value_t *rhs JL_MAYBE_UNROOTED);
JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED) JL_NOTSAFEPOINT;
JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val2(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED, enum jl_partition_kind) JL_NOTSAFEPOINT;
JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED);
JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val2(jl_binding_t *b JL_ROOTING_ARGUMENT, jl_module_t *mod, jl_sym_t *var, jl_value_t *val JL_ROOTED_ARGUMENT JL_MAYBE_UNROOTED, enum jl_partition_kind);
Comment on lines +2006 to +2007
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious this also didn't fail the verifier, since val is marked JL_MAYBE_UNROOTED, but now that means it is clearly not rooted correctly

JL_DLLEXPORT void jl_module_using(jl_module_t *to, jl_module_t *from);
JL_DLLEXPORT void jl_module_use(jl_module_t *to, jl_module_t *from, jl_sym_t *s);
JL_DLLEXPORT void jl_module_use_as(jl_module_t *to, jl_module_t *from, jl_sym_t *s, jl_sym_t *asname);
Expand Down
15 changes: 3 additions & 12 deletions src/julia_internal.h
Original file line number Diff line number Diff line change
Expand Up @@ -886,13 +886,10 @@ EXTERN_INLINE_DECLARE enum jl_partition_kind decode_restriction_kind(jl_ptr_kind
#endif
}

STATIC_INLINE jl_value_t *decode_restriction_value(jl_ptr_kind_union_t pku) JL_NOTSAFEPOINT
STATIC_INLINE jl_value_t *decode_restriction_value(jl_ptr_kind_union_t JL_PROPAGATES_ROOT pku) JL_NOTSAFEPOINT
{
#ifdef _P64
jl_value_t *val = (jl_value_t*)(pku & ~0x7);
// This is a little bit of a lie at the moment - it is one of the things that
// can go wrong with binding replacement.
JL_GC_PROMISE_ROOTED(val);
return val;
#else
return pku.val;
Expand Down Expand Up @@ -926,14 +923,8 @@ STATIC_INLINE int jl_bkind_is_some_guard(enum jl_partition_kind kind) JL_NOTSAFE
return kind == BINDING_KIND_FAILED || kind == BINDING_KIND_GUARD || kind == BINDING_KIND_DECLARED;
}

EXTERN_INLINE_DECLARE jl_binding_partition_t *jl_get_binding_partition(jl_binding_t *b, size_t world) JL_NOTSAFEPOINT {
if (!b)
return NULL;
assert(jl_is_binding(b));
return jl_atomic_load_relaxed(&b->partitions);
}

JL_DLLEXPORT jl_binding_partition_t *jl_get_globalref_partition(jl_globalref_t *gr, size_t world);
JL_DLLEXPORT jl_binding_partition_t *jl_get_binding_partition(jl_binding_t *b JL_PROPAGATES_ROOT, size_t world);
JL_DLLEXPORT jl_binding_partition_t *jl_get_globalref_partition(jl_globalref_t *gr JL_PROPAGATES_ROOT, size_t world);

EXTERN_INLINE_DECLARE uint8_t jl_bpart_get_kind(jl_binding_partition_t *bpart) JL_NOTSAFEPOINT {
return decode_restriction_kind(jl_atomic_load_relaxed(&bpart->restriction));
Expand Down
Loading