World age is expected to produce a delay between a method or Task’s compiled code and their changes to the global state, but it wasn’t clear to me what happens in top-level begin or let blocks. Weirdly, there’s this discrepancy between eval and @eval, and I still don’t know whether these blocks are supposed to commit to a specific world age or not. I haven’t exhaustively wrote versions for all blocks in Julia, but it also applies to for and while.
julia> const pi = 3 # v1.12.2
3
julia> begin
println(pi)
const pi = 3.1 # possible here without manual eval
println(pi) # updated
end;
3
3.1
julia> begin
println(pi)
@eval const pi = 3.14
println(pi) # updated
end;
3.1
3.14
julia> begin
println(pi)
eval(:(const pi = 3.141))
println(pi) # obsolete
end;
3.14
3.14
julia> let
println(pi)
@eval const pi = 3.1415
println(pi) # updated
end;
3.141
3.1415
julia> let
println(pi)
eval(:(const pi = 3.14159))
println(pi) # obsolete
end;
3.1415
3.1415
The first tls_world_age example in the Manual page also changes to show the local world age incrementing if Core.eval is changed to @eval.
julia> function f end
f (generic function with 0 methods)
julia> begin
@show (Int(Base.get_world_counter()), Int(Base.tls_world_age()))
@eval @__MODULE__() f() = 1
@show (Int(Base.get_world_counter()), Int(Base.tls_world_age()))
f()
end
(Int(Base.get_world_counter()), Int(Base.tls_world_age())) = (38693, 38693)
(Int(Base.get_world_counter()), Int(Base.tls_world_age())) = (38694, 38694)
1
The following statements raise the current world age:
…
9. Certain other macros like @eval (depends on the macro implementation)
So this was referring very directly to $(Expr(Symbol("latestworld-if-toplevel"))) as a version of Core.@latestworld that doesn’t error or do anything in function scopes? I guess the question then is why @eval intentionally deviates from eval at top level in 1.12 when it has been described as a simple macro abbreviation by the Metaprogramming page for a while.
Incidentally a harsh reminder that top level isn’t just global scope, but frankly the documentation and implementation are still not clear about what top level is:
julia> function()
struct X end # definitely not top level
end
ERROR: syntax: "struct" expression not at top level
Stacktrace:
[1] top-level scope
@ REPL[22]:1
julia> begin
struct X end # so this is top level?
end
julia> begin
module X end # no?
end
ERROR: syntax: "module" expression not at top level
Stacktrace:
[1] top-level scope
@ REPL[24]:1
julia> function()
Core.@latestworld
end
ERROR: syntax: World age increment not at top level
Stacktrace:
[1] top-level scope
@ REPL[26]:1
julia> begin
Core.@latestworld # same deal as struct
end
I was just trying to be nice to the ecosystem. It was less breaking this way.
1.11 did not have world-age partitioning for bindings, the code in your original post is UB in these versions. However, it’s not entirely related since 1.11 raised world ages after most statements anyway (but was inconsistent about when). The fix for that was separate from binding partitions and was related to the correctness of inference results in global scope.
True, I suppose I’d have to do the zero-argument function call as a pseudo-constant:
julia> pip() = 3 # 1.11
pip (generic function with 1 method)
julia> begin
println(pip())
@eval pip() = 3.1
println(pip()) # updated
end;
3
3.1
julia> begin
println(pip())
eval(:(pip() = 3.14))
println(pip()) # updated
end;
3.1
3.14
which I really hope is just long-unclarified undefined behavior if that can’t happen in 1.12.
I’m pretty grateful that it’s finally moving because the Manual has been ambiguous about MANY fundamentals like this to make room for future core features (at least, I think most people have been surprised that constants weren’t part of world age until now, especially given the zero-argument function workaround). While these don’t seem to cause widespread issues, people have noticed odd implementation inconsistencies, like the difference between importing a function versus a struct, that end up being specified. I think Julia is just going to have to be a little weird until the core features and specification are solid enough to even begin considering a more straightforward v2. I personally would love if explicit-only variable declarations (and thus much simpler conditions for assignment) in v2 could finally let us freely paste code between the global and local scopes and do away with soft scope contexts, and world age bindings would probably be important there.