# World age delay doesn't occur with \`@eval\` in \`begin\` or \`let\` blocks, but does with \`eval\`

**URL:** https://discourse.julialang.org/t/world-age-delay-doesnt-occur-with-eval-in-begin-or-let-blocks-but-does-with-eval/134367
**Category:** General Usage
**Tags:** world\_age
**Created:** [December 4, 2025, 11:56pm UTC](https://discourse.julialang.org/t/world-age-delay-doesnt-occur-with-eval-in-begin-or-let-blocks-but-does-with-eval/134367 "2025-12-04T23:56:13Z")
**Posts on this page:** 7
**Page:** 1

<div class="post-metadata">

### Author: ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)
#### Post date: [December 4, 2025, 11:56pm UTC](https://discourse.julialang.org/t/world-age-delay-doesnt-occur-with-eval-in-begin-or-let-blocks-but-does-with-eval/134367/1 "2025-12-04T23:56:13Z")

</div>

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-auto
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`](https://docs.julialang.org/en/v1/manual/worldage/#World-age-in-general) example in the Manual page also changes to show the local world age incrementing if `Core.eval` is changed to `@eval`.

```julia-auto
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

```

---

<div class="post-metadata">

### Author: ![Keno](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/keno/32/285_2.png) [@Keno](https://discourse.julialang.org/u/Keno)
#### Post date: [December 5, 2025, 3:47am UTC](https://discourse.julialang.org/t/world-age-delay-doesnt-occur-with-eval-in-begin-or-let-blocks-but-does-with-eval/134367/2 "2025-12-05T03:47:31Z")

</div>

`@eval` inserts an explicit world age increment:

```julia-auto
julia> @macroexpand @eval x = 1
:(let var"#1#eval_local_result" = Core.eval(Main, $(Expr(:copyast, :($(QuoteNode(:(x = 1)))))))
      $(Expr(Symbol("latestworld-if-toplevel")))
      var"#1#eval_local_result"
  end)

```

The function version can’t because it’s not syntax.

---

<div class="post-metadata">

### Author: ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)
#### Post date: [December 5, 2025, 9:28am UTC](https://discourse.julialang.org/t/world-age-delay-doesnt-occur-with-eval-in-begin-or-let-blocks-but-does-with-eval/134367/3 "2025-12-05T09:28:11Z")

</div>

> The following statements raise the current world age:  
> …  
> 9. Certain other macros like [`@eval`](https://docs.julialang.org/en/v1/base/base/#Base.@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.

```julia-auto
julia> @macroexpand @eval x=1 # 1.11 and earlier
:(Core.eval(Main, $(Expr(:copyast, :($(QuoteNode(:(x = 1))))))))

```

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-auto
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

```

---

<div class="post-metadata">

### Author: ![Keno](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/keno/32/285_2.png) [@Keno](https://discourse.julialang.org/u/Keno)
#### Post date: [December 5, 2025, 9:39am UTC](https://discourse.julialang.org/t/world-age-delay-doesnt-occur-with-eval-in-begin-or-let-blocks-but-does-with-eval/134367/4 "2025-12-05T09:39:45Z")

</div>

> [@Benny](#):
>
> 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.

I was just trying to be nice to the ecosystem. It was less breaking this way.

> [@Benny](#):
>
> ```julia-auto
> julia> @macroexpand @eval x=1 # 1.11 and earlier
> :(Core.eval(Main, $(Expr(:copyast, :($(QuoteNode(:(x = 1))))))))
> 
> ```

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.

---

<div class="post-metadata">

### Author: ![Keno](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/keno/32/285_2.png) [@Keno](https://discourse.julialang.org/u/Keno)
#### Post date: [December 5, 2025, 9:40am UTC](https://discourse.julialang.org/t/world-age-delay-doesnt-occur-with-eval-in-begin-or-let-blocks-but-does-with-eval/134367/5 "2025-12-05T09:40:08Z")

</div>

> [@Benny](#):
>
> frankly the documentation and implementation are still not clear about what top level is

True - one of the things to clean up after JuliaLowering lands.

---

<div class="post-metadata">

### Author: ![Keno](https://sea2.discourse-cdn.com/julialang/user_avatar/discourse.julialang.org/keno/32/285_2.png) [@Keno](https://discourse.julialang.org/u/Keno)
#### Post date: [December 5, 2025, 9:42am UTC](https://discourse.julialang.org/t/world-age-delay-doesnt-occur-with-eval-in-begin-or-let-blocks-but-does-with-eval/134367/6 "2025-12-05T09:42:13Z")

</div>

Also, hey - I’m proud of myself for actually having documented this :).

---

<div class="post-metadata">

### Author: ![Benny](https://avatars.discourse-cdn.com/v4/letter/b/49beb7/32.png) [@Benny](https://discourse.julialang.org/u/Benny)
#### Post date: [December 5, 2025, 9:58am UTC](https://discourse.julialang.org/t/world-age-delay-doesnt-occur-with-eval-in-begin-or-let-blocks-but-does-with-eval/134367/7 "2025-12-05T09:58:29Z")

</div>

> [@Keno](#):
>
> 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).

True, I suppose I’d have to do the zero-argument function call as a pseudo-constant:

```julia-auto
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.

> [@Keno](#):
>
> I was just trying to be nice to the ecosystem. It was less breaking this way…Also, hey - I’m proud of myself for actually having documented this :).

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`](https://discourse.julialang.org/t/understanding-behavior-of-function-extension-in-1-12/133451), 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.
