diff --git a/Compiler/src/Compiler.jl b/Compiler/src/Compiler.jl index 5c037b0033536..58023a34c833c 100644 --- a/Compiler/src/Compiler.jl +++ b/Compiler/src/Compiler.jl @@ -66,7 +66,7 @@ using Base: @_foldable_meta, @_gc_preserve_begin, @_gc_preserve_end, @nospeciali _uncompressed_ir, datatype_min_ninitialized, partialstruct_init_undefs, fieldcount_noerror, _eval_import, _eval_using, get_ci_mi, get_methodtable, morespecific, specializations, has_image_globalref, - PARTITION_MASK_KIND, PARTITION_KIND_GUARD, PARTITION_FLAG_EXPORTED, PARTITION_FLAG_DEPRECATED, + PARTITION_MASK_KIND, PARTITION_KIND_GUARD, PARTITION_KIND_DECLARED_GUARD, PARTITION_FLAG_EXPORTED, PARTITION_FLAG_DEPRECATED, BINDING_FLAG_ANY_IMPLICIT_EDGES, is_some_implicit, IteratorSize, SizeUnknown, get_require_world, JLOptions, devnull, devnull as stdin diff --git a/Compiler/src/bindinginvalidations.jl b/Compiler/src/bindinginvalidations.jl index 1b84c075d83fc..676def4cc0612 100644 --- a/Compiler/src/bindinginvalidations.jl +++ b/Compiler/src/bindinginvalidations.jl @@ -1,7 +1,7 @@ # This file is a part of Julia. License is MIT: https://julialang.org/license using ..Compiler: _uncompressed_ir, specializations, get_ci_mi, convert, unsafe_load, cglobal, generating_output, has_image_globalref, - PARTITION_MASK_KIND, PARTITION_KIND_GUARD, PARTITION_FLAG_EXPORTED, PARTITION_FLAG_DEPRECATED, + PARTITION_MASK_KIND, PARTITION_KIND_GUARD, PARTITION_KIND_DECLARED_GUARD, PARTITION_FLAG_EXPORTED, PARTITION_FLAG_DEPRECATED, BINDING_FLAG_ANY_IMPLICIT_EDGES, binding_kind, partition_restriction, is_some_imported, is_some_binding_imported, is_some_implicit, SizeUnknown, maybe_add_binding_backedge!, walk_binding_partition, abstract_eval_partition_load, userefs using .Core: SimpleVector, CodeInfo @@ -83,7 +83,7 @@ function invalidate_method_for_globalref!(gr::GlobalRef, method::Method, invalid end export_affecting_partition_flags(bpart::Core.BindingPartition) = - ((bpart.kind & PARTITION_MASK_KIND) == PARTITION_KIND_GUARD, + (let k = bpart.kind & PARTITION_MASK_KIND; k == PARTITION_KIND_GUARD || k == PARTITION_KIND_DECLARED_GUARD end, (bpart.kind & PARTITION_FLAG_EXPORTED) != 0, (bpart.kind & PARTITION_FLAG_DEPRECATED) != 0) diff --git a/base/errorshow.jl b/base/errorshow.jl index 69b67bf36953a..bbe56d499f71d 100644 --- a/base/errorshow.jl +++ b/base/errorshow.jl @@ -1265,7 +1265,7 @@ function UndefVarError_hint(io::IO, ex::UndefVarError) else print(io, "\nSuggestion: check for spelling errors or missing imports.") end - elseif kind === PARTITION_KIND_GLOBAL || kind === PARTITION_KIND_UNDEF_CONST || kind == PARTITION_KIND_DECLARED + elseif kind === PARTITION_KIND_GLOBAL || kind === PARTITION_KIND_UNDEF_CONST || kind == PARTITION_KIND_DECLARED || kind == PARTITION_KIND_DECLARED_GUARD print(io, "\nSuggestion: add an appropriate import or assignment. This global was declared but not assigned.") elseif kind === PARTITION_KIND_FAILED print(io, "\nHint: It looks like two or more modules export different ", diff --git a/base/runtime_internals.jl b/base/runtime_internals.jl index 43ffd3c0703a1..305369a099d73 100644 --- a/base/runtime_internals.jl +++ b/base/runtime_internals.jl @@ -249,6 +249,7 @@ const PARTITION_KIND_DECLARED = 0x8 const PARTITION_KIND_GUARD = 0x9 const PARTITION_KIND_UNDEF_CONST = 0xa const PARTITION_KIND_BACKDATED_CONST = 0xb +const PARTITION_KIND_DECLARED_GUARD = 0xe const PARTITION_FLAG_EXPORTED = 0x10 const PARTITION_FLAG_DEPRECATED = 0x20 @@ -265,10 +266,10 @@ const JL_MODULE_USING_REEXPORT = 0x1 is_defined_const_binding(kind::UInt8) = (kind == PARTITION_KIND_CONST || kind == PARTITION_KIND_CONST_IMPORT || kind == PARTITION_KIND_IMPLICIT_CONST || kind == PARTITION_KIND_BACKDATED_CONST) is_some_const_binding(kind::UInt8) = (is_defined_const_binding(kind) || kind == PARTITION_KIND_UNDEF_CONST) is_some_imported(kind::UInt8) = (kind == PARTITION_KIND_IMPLICIT_GLOBAL || kind == PARTITION_KIND_IMPLICIT_CONST || kind == PARTITION_KIND_EXPLICIT || kind == PARTITION_KIND_IMPORTED) -is_some_implicit(kind::UInt8) = (kind == PARTITION_KIND_IMPLICIT_GLOBAL || kind == PARTITION_KIND_IMPLICIT_CONST || kind == PARTITION_KIND_GUARD || kind == PARTITION_KIND_FAILED) +is_some_implicit(kind::UInt8) = (kind == PARTITION_KIND_IMPLICIT_GLOBAL || kind == PARTITION_KIND_IMPLICIT_CONST || kind == PARTITION_KIND_GUARD || kind == PARTITION_KIND_FAILED || kind == PARTITION_KIND_DECLARED_GUARD) is_some_explicit_imported(kind::UInt8) = (kind == PARTITION_KIND_EXPLICIT || kind == PARTITION_KIND_IMPORTED) is_some_binding_imported(kind::UInt8) = is_some_explicit_imported(kind) || kind == PARTITION_KIND_IMPLICIT_GLOBAL -is_some_guard(kind::UInt8) = (kind == PARTITION_KIND_GUARD || kind == PARTITION_KIND_FAILED || kind == PARTITION_KIND_UNDEF_CONST) +is_some_guard(kind::UInt8) = (kind == PARTITION_KIND_GUARD || kind == PARTITION_KIND_FAILED || kind == PARTITION_KIND_UNDEF_CONST || kind == PARTITION_KIND_DECLARED_GUARD) function lookup_binding_partition(world::UInt, b::Core.Binding) ccall(:jl_get_binding_partition, Ref{Core.BindingPartition}, (Any, UInt), b, world) diff --git a/base/show.jl b/base/show.jl index 68ae347d353bf..59854301e441a 100644 --- a/base/show.jl +++ b/base/show.jl @@ -3356,6 +3356,8 @@ function print_partition(io::IO, partition::Core.BindingPartition) print(io, "undefined const binding") elseif kind == PARTITION_KIND_GUARD print(io, "undefined binding - guard entry") + elseif kind == PARTITION_KIND_DECLARED_GUARD + print(io, "undefined binding - explicitly declared with `public` or `export`") elseif kind == PARTITION_KIND_FAILED print(io, "ambiguous binding - guard entry") elseif kind == PARTITION_KIND_DECLARED diff --git a/src/julia.h b/src/julia.h index 9db400c67cfe0..1d87bc8174618 100644 --- a/src/julia.h +++ b/src/julia.h @@ -667,11 +667,11 @@ typedef struct _jl_weakref_t { // PARTITION_KIND_FAILED // // 2. Weakly Declared Bindings (Weak) -// The binding was declared using `global`. It is treated as a mutable, `Any` type global -// for almost all purposes, except that it receives slightly worse optimizations, since it -// may be replaced. +// The binding was explicitly declared without being given a value. These bindings may be +// replaced by a stronger binding and may be backdated by PARTITION_KIND_BACKDATED_CONST. // -// PARTITION_KIND_DECLARED +// PARTITION_KIND_DECLARED (declared with `global x`) +// PARTITION_KIND_DECLARED_GUARD (declared with `public x` or `export x` without a value) // // 3. Strong Declared Bindings (Weak) // All other bindings are explicitly declared using a keyword or global assignment. @@ -702,6 +702,7 @@ typedef struct _jl_weakref_t { // - PARTITION_KIND_GUARD // - PARTITION_KIND_FAILED // - PARTITION_KIND_DECLARED +// - PARTITION_KIND_DECLARED_GUARD enum jl_partition_kind { // Constant: This binding partition is a constant declared using `const _ = ...` // ->restriction holds the constant value @@ -747,7 +748,15 @@ enum jl_partition_kind { // This is not a real binding kind, but can be used to ask for a re-resolution // of the implicit binding kind PARTITION_FAKE_KIND_IMPLICIT_RECOMPUTE = 0xc, - PARTITION_FAKE_KIND_CYCLE = 0xd + PARTITION_FAKE_KIND_CYCLE = 0xd, + + // Declared Guard: The binding was declared using `public x` or `export x`, but no value + // has been assigned. Unlike PARTITION_KIND_GUARD, this kind is explicit (not implicit): + // it will not be replaced by implicit resolution and blocks implicit import resolution. + // Like PARTITION_KIND_DECLARED, this kind may be replaced by a stronger binding and may + // be backdated by PARTITION_KIND_BACKDATED_CONST. + // ->restriction is NULL + PARTITION_KIND_DECLARED_GUARD = 0xe }; static const uint8_t PARTITION_MASK_KIND = 0x0f; diff --git a/src/julia_internal.h b/src/julia_internal.h index ba63afbe18054..c15dbe88b3480 100644 --- a/src/julia_internal.h +++ b/src/julia_internal.h @@ -1098,7 +1098,7 @@ STATIC_INLINE int jl_bkind_is_some_explicit_import(enum jl_partition_kind kind) } STATIC_INLINE int jl_bkind_is_some_guard(enum jl_partition_kind kind) JL_NOTSAFEPOINT { - return kind == PARTITION_KIND_FAILED || kind == PARTITION_KIND_GUARD; + return kind == PARTITION_KIND_FAILED || kind == PARTITION_KIND_GUARD || kind == PARTITION_KIND_DECLARED_GUARD; } STATIC_INLINE int jl_bkind_is_some_implicit(enum jl_partition_kind kind) JL_NOTSAFEPOINT { diff --git a/src/module.c b/src/module.c index 2e4be681b2a29..592c2320263ee 100644 --- a/src/module.c +++ b/src/module.c @@ -135,8 +135,11 @@ static jl_binding_partition_t *jl_implicit_import_resolved(jl_binding_t *b, stru size_t new_max_world = gap.max_world < resolution.max_world ? gap.max_world : resolution.max_world; size_t new_min_world = gap.min_world > resolution.min_world ? gap.min_world : resolution.min_world; jl_binding_partition_t *next = gap.replace; - if (jl_is_binding_partition(gap.parent)) { - // Check if we can merge this into the previous binding partition + if (jl_is_binding_partition(gap.parent) && !jl_bkind_is_some_guard((enum jl_partition_kind)(new_kind & PARTITION_MASK_KIND))) { + // Check if we can merge this into the previous binding partition. + // Guard partitions are not merged backward: the min_world of the earliest guard + // partition is semantically meaningful (it records the module's creation world, + // i.e. the world at which the using relationships were first established). jl_binding_partition_t *prev = (jl_binding_partition_t *)gap.parent; assert(new_max_world != ~(size_t)0); // It is inconsistent to have a gap with `gap.parent` set, but max_world == ~(size_t)0 size_t expected_prev_min_world = new_max_world + 1; @@ -636,7 +639,10 @@ JL_DLLEXPORT jl_binding_partition_t *jl_declare_constant_val3( size_t prev_bpart_min_world = jl_atomic_load_relaxed(&prev_bpart->min_world); if (prev_bpart_min_world == 0) break; - prev_bpart = jl_get_binding_partition(b, prev_bpart_min_world - 1); + struct implicit_search_gap bdate_gap; + prev_bpart = jl_get_binding_partition_if_present(b, prev_bpart_min_world - 1, &bdate_gap); + if (!prev_bpart) + break; } } // If backdate is required, replace each existing partition by a new one. @@ -1337,7 +1343,7 @@ JL_DLLEXPORT void jl_import_module(jl_task_t *ct, jl_module_t *JL_NONNULL m, jl_ size_t world = jl_atomic_load_acquire(&jl_world_counter); jl_binding_partition_t *bpart = jl_get_binding_partition(b, world); enum jl_partition_kind kind = jl_binding_kind(bpart); - if (!jl_bkind_is_some_implicit(kind) && kind != PARTITION_KIND_DECLARED) { + if (!jl_bkind_is_some_implicit(kind) && kind != PARTITION_KIND_DECLARED && kind != PARTITION_KIND_DECLARED_GUARD) { // Unlike regular constant declaration, we allow this as long as we eventually end up at a constant. jl_walk_binding_inplace(&b, &bpart, world); if (jl_bkind_is_some_constant(jl_binding_kind(bpart))) { @@ -1506,8 +1512,27 @@ int jl_module_public_(jl_module_t *from, jl_sym_t *s, int exported, size_t new_w jl_symbol_name(from->name), jl_symbol_name(s)); } jl_atomic_fetch_or_relaxed(&b->flags, BINDING_FLAG_PUBLICP); - if (was_exported != exported) { - jl_replace_binding_locked2(b, bpart, bpart->restriction, bpart->kind | PARTITION_FLAG_EXPORTED, new_world); + enum jl_partition_kind kind = jl_binding_kind(bpart); + // If the binding is currently an implicit guard (unresolved lookup), replace it with an + // explicit declared-guard, which records that the user explicitly declared this name + // at a known world age, distinguishing it from purely implicit guard partitions. + int needs_declared_guard = (kind == PARTITION_KIND_GUARD); + if (was_exported != exported || needs_declared_guard) { + size_t new_kind; + jl_value_t *restriction; + if (needs_declared_guard) { + new_kind = (size_t)PARTITION_KIND_DECLARED_GUARD; + restriction = NULL; + } + else { + new_kind = (size_t)kind; + restriction = bpart->restriction; + } + // Preserve existing flags, updating the EXPORTED flag as needed + new_kind |= (bpart->kind & PARTITION_MASK_FLAG & ~PARTITION_FLAG_EXPORTED); + if (exported) + new_kind |= PARTITION_FLAG_EXPORTED; + jl_replace_binding_locked2(b, bpart, restriction, new_kind, new_world); return 1; } return 0; @@ -2117,7 +2142,7 @@ void append_module_names(jl_array_t* a, jl_module_t *m, int all, int imported, i jl_bpart_is_exported(bpart->kind) || (imported && (kind == PARTITION_KIND_CONST_IMPORT || kind == PARTITION_KIND_IMPORTED)) || (usings && kind == PARTITION_KIND_EXPLICIT) || - ((kind == PARTITION_KIND_GLOBAL || kind == PARTITION_KIND_CONST || kind == PARTITION_KIND_DECLARED) && (all || main_public))) && + ((kind == PARTITION_KIND_GLOBAL || kind == PARTITION_KIND_CONST || kind == PARTITION_KIND_DECLARED || kind == PARTITION_KIND_DECLARED_GUARD) && (all || main_public))) && (all || (!(bpart->kind & PARTITION_FLAG_DEPRECATED) && !hidden))) _append_symbol_to_bindings_array(a, asname); } diff --git a/src/toplevel.c b/src/toplevel.c index e10cd663c0638..bf8f566ab836e 100644 --- a/src/toplevel.c +++ b/src/toplevel.c @@ -316,7 +316,7 @@ void jl_declare_global(jl_module_t *m, jl_value_t *arg, jl_value_t *set_type, in bpart = jl_get_binding_partition(b, new_world); enum jl_partition_kind kind = jl_binding_kind(bpart); if (kind != PARTITION_KIND_GLOBAL) { - if (jl_bkind_is_some_implicit(kind) || kind == PARTITION_KIND_DECLARED) { + if (jl_bkind_is_some_implicit(kind) || kind == PARTITION_KIND_DECLARED || kind == PARTITION_KIND_DECLARED_GUARD) { if (kind == new_kind) { if (!set_type) goto done; diff --git a/test/rebinding.jl b/test/rebinding.jl index 709cb0859f6f5..56097ef6f3130 100644 --- a/test/rebinding.jl +++ b/test/rebinding.jl @@ -469,3 +469,84 @@ module ReexportTests end @test User3.same_name == 42 end + +# Test PARTITION_KIND_DECLARED_GUARD: `export` or `public` on an undefined name creates +# a DECLARED_GUARD partition rather than leaving the binding as an implicit GUARD. +module DeclaredGuardTests + using Test + + # `export` on an undefined name creates DECLARED_GUARD + export exported_undef + @test Base.binding_kind(@__MODULE__, :exported_undef) == Base.PARTITION_KIND_DECLARED_GUARD + @test Base.is_some_guard(Base.PARTITION_KIND_DECLARED_GUARD) + @test Base.is_some_implicit(Base.PARTITION_KIND_DECLARED_GUARD) + + # `public` on an undefined name also creates DECLARED_GUARD + public public_undef + @test Base.binding_kind(@__MODULE__, :public_undef) == Base.PARTITION_KIND_DECLARED_GUARD + + # The binding shows the right description via print_partition + bpart = Base.lookup_binding_partition(Base.tls_world_age(), GlobalRef(@__MODULE__, :exported_undef)) + @test occursin("explicitly declared", sprint(Base.print_partition, bpart)) + + # The exported name appears in names() (exported = true by default with export keyword) + @test :exported_undef in names(@__MODULE__) + + # UndefVarError hint says "declared but not assigned" for DECLARED_GUARD + Base.Experimental.register_error_hint(Base.UndefVarError_hint, UndefVarError) + ex = try; @eval exported_undef; catch e; e; end + @test ex isa UndefVarError + @test occursin("declared but not assigned", sprint(Base.showerror, ex)) + + # DECLARED_GUARD is re-resolvable via implicit import when a source binding appears + module DeclGuardSource + export resolvable_sym + end + module DeclGuardConsumer + using Test + using ..DeclGuardSource + export resolvable_sym # creates DECLARED_GUARD + @test Base.binding_kind(@__MODULE__, :resolvable_sym) == Base.PARTITION_KIND_DECLARED_GUARD + Core.eval(DeclGuardSource, :(resolvable_sym = 99)) + @test resolvable_sym == 99 + @test Base.is_some_imported(Base.binding_kind(@__MODULE__, :resolvable_sym)) + end +end + +@testset "Rebinding worlds" begin + # Test that partition lists preserve the world age of creation + w0 = Base.get_world_counter() + M = @eval module $(gensym()) + export x + public y + end + w = Base.get_world_counter() + @eval M begin + const x = 1 + const y = 2 + end + bx = convert(Core.Binding, GlobalRef(M, :x)) + by = convert(Core.Binding, GlobalRef(M, :y)) + px = bx.partitions + @test px.min_world > w + @test (px.kind & Base.PARTITION_FLAG_EXPORTED) != 0 + @test (px.kind & Base.PARTITION_MASK_KIND) == Base.PARTITION_KIND_CONST + px = px.next + @test w0 + 1 < px.min_world < w + @test px.max_world == w + @test (px.kind & Base.PARTITION_FLAG_EXPORTED) != 0 + @test (px.kind & Base.PARTITION_MASK_KIND) == Base.PARTITION_KIND_BACKDATED_CONST + px = px.next + @test w0 + 1 == px.min_world + py = by.partitions + @test py.min_world > w + @test (py.kind & Base.PARTITION_FLAG_EXPORTED) == 0 + @test (py.kind & Base.PARTITION_MASK_KIND) == Base.PARTITION_KIND_CONST + py = py.next + @test w0 + 1 < py.min_world <= w + @test py.max_world == w + 1 + @test (py.kind & Base.PARTITION_FLAG_EXPORTED) == 0 + @test (py.kind & Base.PARTITION_MASK_KIND) == Base.PARTITION_KIND_BACKDATED_CONST + py = py.next + @test w0 + 1 == py.min_world +end diff --git a/test/worlds.jl b/test/worlds.jl index 96685129c9e99..bab2e70aea0c1 100644 --- a/test/worlds.jl +++ b/test/worlds.jl @@ -616,3 +616,42 @@ end # Test that the hash function works without world age issues @test hash(bar59429, UInt(0)) isa UInt end + +# Test that GUARD partition min_world is preserved as the module's creation timestamp +# and is not backdated when the binding is accessed from older worlds. +let world_before = Base.tls_world_age() + @eval module GuardMinWorldPreserveModule end + b = convert(Core.Binding, GlobalRef(GuardMinWorldPreserveModule, :undef_sym)) + bpart = Base.lookup_binding_partition(Base.tls_world_age(), b) + guard_min_world = bpart.min_world + @test Base.binding_kind(bpart) == Base.PARTITION_KIND_GUARD + @test guard_min_world > world_before # reflects module creation world, not 0 + # Accessing at an older world (before module creation) previously merged the + # GUARD partition backward, erasing the meaningful min_world boundary. + Base.lookup_binding_partition(UInt(world_before), b) + bpart2 = Base.lookup_binding_partition(Base.tls_world_age(), b) + @test Base.binding_kind(bpart2) == Base.PARTITION_KIND_GUARD + @test bpart2.min_world == guard_min_world # min_world must not be backdated to 0 +end + +# Test that the backdate loop in jl_declare_constant_val3 does not extend +# BACKDATED_CONST to worlds before the module existed. +# Previously, jl_get_binding_partition in the backdate loop triggered implicit +# resolution, creating pre-creation GUARD partitions that caused BACKDATED_CONST +# to cover worlds where the module did not yet exist. +let + @eval module BackdateLoopGuardModule end + module_creation_world = Base.tls_world_age() + # Declare a self-referential struct, which triggers jl_declare_constant_val3's + # backdate loop. + Core.eval(BackdateLoopGuardModule, quote + struct BackdateSelfRef + x::Vector{BackdateSelfRef} + BackdateSelfRef() = new(BackdateSelfRef[]) + end + end) + b = convert(Core.Binding, GlobalRef(BackdateLoopGuardModule, :BackdateSelfRef)) + # At world 0 (before the module existed), the binding must not be BACKDATED_CONST + bpart_at_0 = Base.lookup_binding_partition(UInt(0), b) + @test Base.binding_kind(bpart_at_0) != Base.PARTITION_KIND_BACKDATED_CONST +end