Skip to content

Commit a95ff03

Browse files
committed
bpart: Give a warning when accessing a backdated const binding
This implements the strategy proposed in #57102 (comment). Example: ``` julia> function foo(i) eval(:(const x = $i)) x end foo (generic function with 1 method) julia> foo(1) WARNING: Detected access to binding Main.x in a world prior to its definition world. Julia 1.12 has introduced more strict world age semantics for global bindings. !!! This code may malfunction under Revise. !!! This code will error in future versions of Julia. Hint: Add an appropriate `invokelatest` around the access to this binding. 1 ``` The warning is triggered once per binding to avoid spamming for repeated access.
1 parent 61e8f1d commit a95ff03

18 files changed

+147
-63
lines changed

Compiler/src/Compiler.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ using Core: ABIOverride, Builtin, CodeInstance, IntrinsicFunction, MethodInstanc
4949

5050
using Base
5151
using Base: @_foldable_meta, @_gc_preserve_begin, @_gc_preserve_end, @nospecializeinfer,
52-
BINDING_KIND_GLOBAL, BINDING_KIND_UNDEF_CONST, Base, BitVector, Bottom, Callable, DataTypeFieldDesc,
52+
BINDING_KIND_GLOBAL, BINDING_KIND_UNDEF_CONST, BINDING_KIND_BACKDATED_CONST,
53+
Base, BitVector, Bottom, Callable, DataTypeFieldDesc,
5354
EffectsOverride, Filter, Generator, IteratorSize, JLOptions, NUM_EFFECTS_OVERRIDES,
5455
OneTo, Ordering, RefValue, SizeUnknown, _NAMEDTUPLE_NAME,
5556
_array_for, _bits_findnext, _methods_by_ftype, _uniontypes, all, allocatedinline, any,

Compiler/src/abstractinterpretation.jl

+5
Original file line numberDiff line numberDiff line change
@@ -3524,6 +3524,11 @@ function abstract_eval_partition_load(interp::AbstractInterpreter, partition::Co
35243524
end
35253525

35263526
if is_defined_const_binding(kind)
3527+
if kind == BINDING_KIND_BACKDATED_CONST
3528+
# Infer this as guard. We do not want a later const definition to retroactively improve
3529+
# inference results in an earlier world.
3530+
return RTEffects(Any, UndefVarError, generic_getglobal_effects)
3531+
end
35273532
rt = Const(partition_restriction(partition))
35283533
return RTEffects(rt, Union{}, Effects(EFFECTS_TOTAL, inaccessiblememonly=is_mutation_free_argtype(rt) ? ALWAYS_TRUE : ALWAYS_FALSE))
35293534
end

Compiler/src/ssair/slot2ssa.jl

-4
Original file line numberDiff line numberDiff line change
@@ -137,10 +137,6 @@ function fixemup!(@specialize(slot_filter), @specialize(rename_slot), ir::IRCode
137137
return nothing
138138
end
139139
op[] = x
140-
elseif isa(val, GlobalRef) && !(isdefined(val.mod, val.name) && isconst(val.mod, val.name))
141-
typ = typ_for_val(val, ci, ir, idx, Any[])
142-
new_inst = NewInstruction(val, typ)
143-
op[] = NewSSAValue(insert_node!(ir, idx, new_inst).id - length(ir.stmts))
144140
elseif isexpr(val, :static_parameter)
145141
ty = typ_for_val(val, ci, ir, idx, Any[])
146142
if isa(ty, Const)

Compiler/test/invalidation.jl

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ let mi = Base.method_instance(basic_caller, (Float64,))
5555
end
5656

5757
# this redefinition below should invalidate the cache
58-
const BASIC_CALLER_WORLD = Base.get_world_counter()
58+
const BASIC_CALLER_WORLD = Base.get_world_counter()+1
5959
basic_callee(x) = x, x
6060
@test !isdefined(Base.method_instance(basic_callee, (Float64,)), :cache)
6161
let mi = Base.method_instance(basic_caller, (Float64,))

base/Base_compiler.jl

+4
Original file line numberDiff line numberDiff line change
@@ -257,6 +257,10 @@ using .Order
257257
include("coreir.jl")
258258
include("invalidation.jl")
259259

260+
# Because lowering inserts direct references, it is mandatory for this binding
261+
# to exist before we start inferring code.
262+
function string end
263+
260264
# For OS specific stuff
261265
# We need to strcat things here, before strings are really defined
262266
function strcat(x::String, y::String)

base/boot.jl

+5-3
Original file line numberDiff line numberDiff line change
@@ -383,9 +383,10 @@ struct StackOverflowError <: Exception end
383383
struct UndefRefError <: Exception end
384384
struct UndefVarError <: Exception
385385
var::Symbol
386+
world::UInt64
386387
scope # a Module or Symbol or other object describing the context where this variable was looked for (e.g. Main or :local or :static_parameter)
387-
UndefVarError(var::Symbol) = new(var)
388-
UndefVarError(var::Symbol, @nospecialize scope) = new(var, scope)
388+
UndefVarError(var::Symbol) = new(var, ccall(:jl_get_tls_world_age, UInt, ()))
389+
UndefVarError(var::Symbol, @nospecialize scope) = new(var, ccall(:jl_get_tls_world_age, UInt, ()), scope)
389390
end
390391
struct ConcurrencyViolationError <: Exception
391392
msg::AbstractString
@@ -717,7 +718,8 @@ macro __doc__(x)
717718
end
718719

719720
isbasicdoc(@nospecialize x) = (isa(x, Expr) && x.head === :.) || isa(x, Union{QuoteNode, Symbol})
720-
iscallexpr(ex::Expr) = (isa(ex, Expr) && ex.head === :where) ? iscallexpr(ex.args[1]) : (isa(ex, Expr) && ex.head === :call)
721+
firstarg(arg1, args...) = arg1
722+
iscallexpr(ex::Expr) = (isa(ex, Expr) && ex.head === :where) ? iscallexpr(firstarg(ex.args...)) : (isa(ex, Expr) && ex.head === :call)
721723
iscallexpr(ex) = false
722724
function ignoredoc(source, mod, str, expr)
723725
(isbasicdoc(expr) || iscallexpr(expr)) && return Expr(:escape, nothing)

base/docs/bindings.jl

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,8 @@ end
1616

1717
bindingexpr(x) = Expr(:call, Binding, splitexpr(x)...)
1818

19-
defined(b::Binding) = isdefined(b.mod, b.var)
20-
resolve(b::Binding) = getfield(b.mod, b.var)
19+
defined(b::Binding) = invokelatest(isdefined, b.mod, b.var)
20+
resolve(b::Binding) = invokelatest(getfield, b.mod, b.var)
2121

2222
function splitexpr(x::Expr)
2323
isexpr(x, :macrocall) ? splitexpr(x.args[1]) :

base/runtime_internals.jl

+2-1
Original file line numberDiff line numberDiff line change
@@ -229,8 +229,9 @@ const BINDING_KIND_FAILED = 0x6
229229
const BINDING_KIND_DECLARED = 0x7
230230
const BINDING_KIND_GUARD = 0x8
231231
const BINDING_KIND_UNDEF_CONST = 0x9
232+
const BINDING_KIND_BACKDATED_CONST = 0xa
232233

233-
is_defined_const_binding(kind::UInt8) = (kind == BINDING_KIND_CONST || kind == BINDING_KIND_CONST_IMPORT)
234+
is_defined_const_binding(kind::UInt8) = (kind == BINDING_KIND_CONST || kind == BINDING_KIND_CONST_IMPORT || kind == BINDING_KIND_BACKDATED_CONST)
234235
is_some_const_binding(kind::UInt8) = (is_defined_const_binding(kind) || kind == BINDING_KIND_UNDEF_CONST)
235236
is_some_imported(kind::UInt8) = (kind == BINDING_KIND_IMPLICIT || kind == BINDING_KIND_EXPLICIT || kind == BINDING_KIND_IMPORTED)
236237
is_some_guard(kind::UInt8) = (kind == BINDING_KIND_GUARD || kind == BINDING_KIND_DECLARED || kind == BINDING_KIND_FAILED || kind == BINDING_KIND_UNDEF_CONST)

base/show.jl

+10-7
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ function _isself(ft::DataType)
3535
isdefined(ftname, :mt) || return false
3636
name = ftname.mt.name
3737
mod = parentmodule(ft) # NOTE: not necessarily the same as ft.name.mt.module
38-
return isdefined(mod, name) && ft == typeof(getfield(mod, name))
38+
return invokelatest(isdefinedglobal, mod, name) && ft == typeof(invokelatest(getglobal, mod, name))
3939
end
4040

4141
function show(io::IO, ::MIME"text/plain", f::Function)
@@ -542,8 +542,8 @@ function show_function(io::IO, f::Function, compact::Bool, fallback::Function)
542542
fallback(io, f)
543543
elseif compact
544544
print(io, mt.name)
545-
elseif isdefined(mt, :module) && isdefined(mt.module, mt.name) &&
546-
getfield(mt.module, mt.name) === f
545+
elseif isdefined(mt, :module) && isdefinedglobal(mt.module, mt.name) &&
546+
getglobal(mt.module, mt.name) === f
547547
# this used to call the removed internal function `is_exported_from_stdlib`, which effectively
548548
# just checked for exports from Core and Base.
549549
mod = get(io, :module, UsesCoreAndBaseOnly)
@@ -1025,15 +1025,15 @@ function isvisible(sym::Symbol, parent::Module, from::Module)
10251025
from_owner = ccall(:jl_binding_owner, Ptr{Cvoid}, (Any, Any), from, sym)
10261026
return owner !== C_NULL && from_owner === owner &&
10271027
!isdeprecated(parent, sym) &&
1028-
isdefined(from, sym) # if we're going to return true, force binding resolution
1028+
isdefinedglobal(from, sym) # if we're going to return true, force binding resolution
10291029
end
10301030

10311031
function is_global_function(tn::Core.TypeName, globname::Union{Symbol,Nothing})
10321032
if globname !== nothing
10331033
globname_str = string(globname::Symbol)
10341034
if ('#' globname_str && '@' globname_str && isdefined(tn, :module) &&
1035-
isbindingresolved(tn.module, globname) && isdefined(tn.module, globname) &&
1036-
isconcretetype(tn.wrapper) && isa(getfield(tn.module, globname), tn.wrapper))
1035+
isbindingresolved(tn.module, globname) && isdefinedglobal(tn.module, globname) &&
1036+
isconcretetype(tn.wrapper) && isa(getglobal(tn.module, globname), tn.wrapper))
10371037
return true
10381038
end
10391039
end
@@ -3364,7 +3364,10 @@ function print_partition(io::IO, partition::Core.BindingPartition)
33643364
end
33653365
print(io, " - ")
33663366
kind = binding_kind(partition)
3367-
if is_defined_const_binding(kind)
3367+
if kind == BINDING_KIND_BACKDATED_CONST
3368+
print(io, "backdated constant binding to ")
3369+
print(io, partition_restriction(partition))
3370+
elseif is_defined_const_binding(kind)
33683371
print(io, "constant binding to ")
33693372
print(io, partition_restriction(partition))
33703373
elseif kind == BINDING_KIND_UNDEF_CONST

doc/src/manual/methods.md

+3-2
Original file line numberDiff line numberDiff line change
@@ -587,12 +587,13 @@ In the example above, we see that the "current world" (in which the method `newf
587587
is one greater than the task-local "runtime world" that was fixed when the execution of `tryeval` started.
588588

589589
Sometimes it is necessary to get around this (for example, if you are implementing the above REPL).
590-
Fortunately, there is an easy solution: call the function using [`Base.invokelatest`](@ref):
590+
Fortunately, there is an easy solution: call the function using [`Base.invokelatest`](@ref) or
591+
the macro version [`Base.@invokelatest`](@ref):
591592

592593
```jldoctest
593594
julia> function tryeval2()
594595
@eval newfun2() = 2
595-
Base.invokelatest(newfun2)
596+
@invokelatest newfun2()
596597
end
597598
tryeval2 (generic function with 1 method)
598599

src/codegen.cpp

+2-1
Original file line numberDiff line numberDiff line change
@@ -3467,7 +3467,8 @@ static jl_cgval_t emit_globalref(jl_codectx_t &ctx, jl_module_t *mod, jl_sym_t *
34673467
break;
34683468
pku = jl_atomic_load_acquire(&bpart->restriction);
34693469
}
3470-
if (bpart && jl_bkind_is_some_constant(decode_restriction_kind(pku))) {
3470+
enum jl_partition_kind kind = decode_restriction_kind(pku);
3471+
if (bpart && (jl_bkind_is_some_constant(kind) && kind != BINDING_KIND_BACKDATED_CONST)) {
34713472
jl_value_t *constval = decode_restriction_value(pku);
34723473
if (!constval) {
34733474
undef_var_error_ifnot(ctx, ConstantInt::get(getInt1Ty(ctx.builder.getContext()), 0), name, (jl_value_t*)mod);

src/julia.h

+15-7
Original file line numberDiff line numberDiff line change
@@ -647,7 +647,10 @@ enum jl_partition_kind {
647647
// Undef Constant: This binding partition is a constant declared using `const`, but
648648
// without a value.
649649
// ->restriction is NULL
650-
BINDING_KIND_UNDEF_CONST = 0x9
650+
BINDING_KIND_UNDEF_CONST = 0x9,
651+
// Backated constant. A constant that was backdated for compatibility. In all other
652+
// ways equivalent to BINDING_KIND_CONST, but prints a warning on access
653+
BINDING_KIND_BACKDATED_CONST = 0xa,
651654
};
652655

653656
#ifdef _P64
@@ -693,7 +696,7 @@ typedef struct _jl_binding_t {
693696
jl_globalref_t *globalref; // cached GlobalRef for this binding
694697
_Atomic(jl_value_t*) value;
695698
_Atomic(jl_binding_partition_t*) partitions;
696-
uint8_t declared:1;
699+
uint8_t did_print_backdate_admonition:1;
697700
uint8_t exportp:1; // `public foo` sets `publicp`, `export foo` sets both `publicp` and `exportp`
698701
uint8_t publicp:1; // exportp without publicp is not allowed.
699702
uint8_t deprecated:2; // 0=not deprecated, 1=renamed, 2=moved to another package
@@ -2025,10 +2028,6 @@ JL_DLLEXPORT void jl_module_public(jl_module_t *from, jl_sym_t *s, int exported)
20252028
JL_DLLEXPORT int jl_is_imported(jl_module_t *m, jl_sym_t *s);
20262029
JL_DLLEXPORT int jl_module_exports_p(jl_module_t *m, jl_sym_t *var);
20272030
JL_DLLEXPORT void jl_add_standard_imports(jl_module_t *m);
2028-
STATIC_INLINE jl_function_t *jl_get_function(jl_module_t *m, const char *name)
2029-
{
2030-
return (jl_function_t*)jl_get_global(m, jl_symbol(name));
2031-
}
20322031

20332032
// eq hash tables
20342033
JL_DLLEXPORT jl_genericmemory_t *jl_eqtable_put(jl_genericmemory_t *h JL_ROOTING_ARGUMENT, jl_value_t *key, jl_value_t *val JL_ROOTED_ARGUMENT, int *inserted);
@@ -2587,9 +2586,18 @@ typedef struct {
25872586
} jl_nullable_float32_t;
25882587

25892588
#define jl_root_task (jl_current_task->ptls->root_task)
2590-
25912589
JL_DLLEXPORT jl_task_t *jl_get_current_task(void) JL_GLOBALLY_ROOTED JL_NOTSAFEPOINT;
25922590

2591+
STATIC_INLINE jl_function_t *jl_get_function(jl_module_t *m, const char *name)
2592+
{
2593+
jl_task_t *ct = jl_get_current_task();
2594+
size_t last_world = ct->world_age;
2595+
ct->world_age = jl_get_world_counter();
2596+
jl_value_t *r = jl_get_global(m, jl_symbol(name));
2597+
ct->world_age = last_world;
2598+
return (jl_function_t*)r;
2599+
}
2600+
25932601
// TODO: we need to pin the task while using this (set pure bit)
25942602
JL_DLLEXPORT jl_jmp_buf *jl_get_safe_restore(void) JL_NOTSAFEPOINT;
25952603
JL_DLLEXPORT void jl_set_safe_restore(jl_jmp_buf *) JL_NOTSAFEPOINT;

src/julia_internal.h

+9-3
Original file line numberDiff line numberDiff line change
@@ -933,6 +933,10 @@ EXTERN_INLINE_DECLARE enum jl_partition_kind decode_restriction_kind(jl_ptr_kind
933933
if (bits == BINDING_KIND_CONST) {
934934
return BINDING_KIND_UNDEF_CONST;
935935
}
936+
} else {
937+
if (bits == BINDING_KIND_DECLARED) {
938+
return BINDING_KIND_BACKDATED_CONST;
939+
}
936940
}
937941

938942
return (enum jl_partition_kind)bits;
@@ -956,12 +960,14 @@ STATIC_INLINE jl_ptr_kind_union_t encode_restriction(jl_value_t *val, enum jl_pa
956960
#ifdef _P64
957961
if (kind == BINDING_KIND_GUARD || kind == BINDING_KIND_DECLARED || kind == BINDING_KIND_FAILED || kind == BINDING_KIND_UNDEF_CONST)
958962
assert(val == NULL);
959-
else if (kind == BINDING_KIND_IMPLICIT || kind == BINDING_KIND_CONST)
963+
else if (kind == BINDING_KIND_IMPLICIT || kind == BINDING_KIND_CONST || kind == BINDING_KIND_BACKDATED_CONST)
960964
assert(val != NULL);
961965
if (kind == BINDING_KIND_GUARD)
962966
kind = BINDING_KIND_IMPLICIT;
963967
else if (kind == BINDING_KIND_UNDEF_CONST)
964968
kind = BINDING_KIND_CONST;
969+
else if (kind == BINDING_KIND_BACKDATED_CONST)
970+
kind = BINDING_KIND_DECLARED;
965971
assert((((uintptr_t)val) & 0x7) == 0);
966972
return ((jl_ptr_kind_union_t)val) | kind;
967973
#else
@@ -975,11 +981,11 @@ STATIC_INLINE int jl_bkind_is_some_import(enum jl_partition_kind kind) JL_NOTSAF
975981
}
976982

977983
STATIC_INLINE int jl_bkind_is_some_constant(enum jl_partition_kind kind) JL_NOTSAFEPOINT {
978-
return kind == BINDING_KIND_CONST || kind == BINDING_KIND_CONST_IMPORT || kind == BINDING_KIND_UNDEF_CONST;
984+
return kind == BINDING_KIND_CONST || kind == BINDING_KIND_CONST_IMPORT || kind == BINDING_KIND_UNDEF_CONST || kind == BINDING_KIND_BACKDATED_CONST;
979985
}
980986

981987
STATIC_INLINE int jl_bkind_is_defined_constant(enum jl_partition_kind kind) JL_NOTSAFEPOINT {
982-
return kind == BINDING_KIND_CONST || kind == BINDING_KIND_CONST_IMPORT;
988+
return kind == BINDING_KIND_CONST || kind == BINDING_KIND_CONST_IMPORT || kind == BINDING_KIND_BACKDATED_CONST;
983989
}
984990

985991
STATIC_INLINE int jl_bkind_is_some_guard(enum jl_partition_kind kind) JL_NOTSAFEPOINT {

0 commit comments

Comments
 (0)